diff --git a/lib/bones73k/accounts.ex b/lib/bones73k/accounts.ex index 6e2d9a1d..d0914c0f 100644 --- a/lib/bones73k/accounts.ex +++ b/lib/bones73k/accounts.ex @@ -172,10 +172,10 @@ defmodule Bones73k.Accounts do {:error, %Ecto.Changeset{}} """ - def apply_user_email(user, password, attrs) do + def apply_user_email(user, %{"current_password" => curr_pw} = attrs) do user |> User.email_changeset(attrs) - |> User.validate_current_password(password) + |> User.validate_current_password(curr_pw) |> Ecto.Changeset.apply_action(:update) end @@ -247,11 +247,11 @@ defmodule Bones73k.Accounts do {:error, %Ecto.Changeset{}} """ - def update_user_password(user, password, attrs) do + def update_user_password(user, %{"current_password" => curr_pw} = attrs) do changeset = user |> User.password_changeset(attrs) - |> User.validate_current_password(password) + |> User.validate_current_password(curr_pw) Ecto.Multi.new() |> Ecto.Multi.update(:user, changeset) diff --git a/lib/bones73k_web/controllers/user_auth.ex b/lib/bones73k_web/controllers/user_auth.ex index c7c280e1..30fbd50a 100644 --- a/lib/bones73k_web/controllers/user_auth.ex +++ b/lib/bones73k_web/controllers/user_auth.ex @@ -35,10 +35,6 @@ defmodule Bones73kWeb.UserAuth do |> put_session(:user_token, token) |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") |> maybe_write_remember_me_cookie(token, params) - |> put_flash( - :info, - raw("Welcome back, #{user.email} — you were logged in successfuly.") - ) |> redirect(to: get_session(conn, :user_return_to) || signed_in_path(conn)) end diff --git a/lib/bones73k_web/controllers/user_session_controller.ex b/lib/bones73k_web/controllers/user_session_controller.ex index ecc84365..546943a4 100644 --- a/lib/bones73k_web/controllers/user_session_controller.ex +++ b/lib/bones73k_web/controllers/user_session_controller.ex @@ -1,6 +1,7 @@ defmodule Bones73kWeb.UserSessionController do use Bones73kWeb, :controller + alias Phoenix.HTML alias Bones73k.Accounts alias Bones73k.Accounts.User alias Bones73kWeb.UserAuth @@ -11,7 +12,12 @@ defmodule Bones73kWeb.UserSessionController do def create(conn, %{"user" => %{"email" => email, "password" => password} = user_params}) do if user = Accounts.get_user_by_email_and_password(email, password) do - UserAuth.log_in_user(conn, user, user_params) + conn + |> put_flash( + :info, + HTML.raw("Welcome back, #{user.email} — you were logged in successfuly.") + ) + |> UserAuth.log_in_user(user, user_params) else render(conn, "new.html", error_message: "Invalid email or password") end diff --git a/lib/bones73k_web/controllers/user_settings_controller.ex b/lib/bones73k_web/controllers/user_settings_controller.ex index a2e9b7eb..4aefacc0 100644 --- a/lib/bones73k_web/controllers/user_settings_controller.ex +++ b/lib/bones73k_web/controllers/user_settings_controller.ex @@ -2,36 +2,6 @@ defmodule Bones73kWeb.UserSettingsController do use Bones73kWeb, :controller alias Bones73k.Accounts - alias Bones73kWeb.UserAuth - - plug(:assign_email_and_password_changesets) - - def edit(conn, _params) do - render(conn, "edit.html") - end - - def update_email(conn, %{"current_password" => password, "user" => user_params}) do - user = conn.assigns.current_user - - case Accounts.apply_user_email(user, password, user_params) do - {:ok, applied_user} -> - Accounts.deliver_update_email_instructions( - applied_user, - user.email, - &Routes.user_settings_url(conn, :confirm_email, &1) - ) - - conn - |> put_flash( - :info, - "A link to confirm your email change has been sent to the new address." - ) - |> redirect(to: Routes.user_settings_path(conn, :edit)) - - {:error, changeset} -> - render(conn, "edit.html", email_changeset: changeset) - end - end def confirm_email(conn, %{"token" => token}) do case Accounts.update_user_email(conn.assigns.current_user, token) do @@ -46,27 +16,4 @@ defmodule Bones73kWeb.UserSettingsController do |> redirect(to: Routes.user_settings_path(conn, :edit)) end end - - def update_password(conn, %{"current_password" => password, "user" => user_params}) do - user = conn.assigns.current_user - - case Accounts.update_user_password(user, password, user_params) do - {:ok, user} -> - conn - |> put_flash(:info, "Password updated successfully.") - |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit)) - |> UserAuth.log_in_user(user) - - {:error, changeset} -> - render(conn, "edit.html", password_changeset: changeset) - end - end - - defp assign_email_and_password_changesets(conn, _opts) do - user = conn.assigns.current_user - - conn - |> assign(:email_changeset, Accounts.change_user_email(user)) - |> assign(:password_changeset, Accounts.change_user_password(user)) - end end diff --git a/lib/bones73k_web/live/user/registration.ex b/lib/bones73k_web/live/user/registration.ex index 6e990fc0..0ad56c70 100644 --- a/lib/bones73k_web/live/user/registration.ex +++ b/lib/bones73k_web/live/user/registration.ex @@ -4,12 +4,6 @@ defmodule Bones73kWeb.UserLive.Registration do alias Bones73k.Accounts alias Bones73k.Accounts.User - @messages [ - success: "Welcome! New accout created.", - info: - "Some features may be unavailable until you confirm your email. Check your inbox for instructions." - ] - @impl true def mount(_params, session, socket) do socket @@ -25,7 +19,11 @@ defmodule Bones73kWeb.UserLive.Registration do %{ user_id: nil, user_return_to: Map.get(session, "user_return_to", "/"), - messages: @messages + messages: [ + success: "Welcome! Your new account has been created, and you've been logged in.", + info: + "Some features may be unavailable until you confirm your email address. Check your inbox for instructions." + ] } end diff --git a/lib/bones73k_web/live/user/reset_password.ex b/lib/bones73k_web/live/user/reset_password.ex index c3c9f041..b7d751b4 100644 --- a/lib/bones73k_web/live/user/reset_password.ex +++ b/lib/bones73k_web/live/user/reset_password.ex @@ -27,7 +27,7 @@ defmodule Bones73kWeb.UserLive.ResetPassword do {:ok, _} -> {:noreply, socket - |> put_flash(:success, "Password reset successfully.") + |> put_flash(:info, "Password reset successfully.") |> redirect(to: Routes.user_session_path(socket, :new))} {:error, changeset} -> diff --git a/lib/bones73k_web/live/user/settings.ex b/lib/bones73k_web/live/user/settings.ex new file mode 100644 index 00000000..2821f362 --- /dev/null +++ b/lib/bones73k_web/live/user/settings.ex @@ -0,0 +1,39 @@ +defmodule Bones73kWeb.UserLive.Settings do + use Bones73kWeb, :live_view + + alias Bones73k.Accounts.User + + @impl true + def mount(_params, session, socket) do + socket + |> assign_defaults(session) + |> alert_email_verified?() + |> live_okreply() + end + + defp alert_email_verified?(socket) do + case socket.assigns.current_user do + %{confirmed_at: nil} -> + put_flash(socket, :warning, [ + "Your email hasn't been confirmed, some areas may be restricted. Shall we ", + link("resend the verification email?", + to: Routes.user_confirmation_path(socket, :new), + class: "alert-link" + ) + ]) + + _ -> + socket + end + end + + @impl true + def handle_info({:put_flash_message, {flash_type, msg}}, socket) do + socket |> put_flash(flash_type, msg) |> live_noreply() + end + + @impl true + def handle_info({:clear_flash_message, flash_type}, socket) do + socket |> clear_flash(flash_type) |> live_noreply() + end +end diff --git a/lib/bones73k_web/live/user/settings.html.leex b/lib/bones73k_web/live/user/settings.html.leex new file mode 100644 index 00000000..994b1fcb --- /dev/null +++ b/lib/bones73k_web/live/user/settings.html.leex @@ -0,0 +1,9 @@ +

+ <%= icon_div @socket, "bi-sliders", [class: "icon baseline"] %> + User Settings +

+ +
+ <%= live_component @socket, Bones73kWeb.UserLive.Settings.Email, id: "email-#{@current_user.id}", current_user: @current_user %> + <%= live_component @socket, Bones73kWeb.UserLive.Settings.Password, id: "password-#{@current_user.id}", current_user: @current_user %> +
diff --git a/lib/bones73k_web/live/user/settings/email.ex b/lib/bones73k_web/live/user/settings/email.ex new file mode 100644 index 00000000..341ed0ff --- /dev/null +++ b/lib/bones73k_web/live/user/settings/email.ex @@ -0,0 +1,62 @@ +defmodule Bones73kWeb.UserLive.Settings.Email do + use Bones73kWeb, :live_component + + alias Bones73k.Accounts + alias Bones73k.Accounts.User + + @impl true + def update(%{current_user: user} = assigns, socket) do + socket + |> assign(id: assigns.id) + |> assign(current_user: user) + |> assign(changeset: get_changeset(user)) + |> live_okreply() + end + + defp get_changeset(user, user_params \\ %{}) do + Accounts.change_user_email(user, user_params) + end + + @impl true + def handle_event("validate", %{"user" => user_params}, socket) do + cs = get_changeset(socket.assigns.current_user, user_params) + {:noreply, assign(socket, changeset: %{cs | action: :update})} + end + + # user_settings_path GET /users/settings/confirm_email/:token Bones73kWeb.UserSettingsController :confirm_email + + @impl true + def handle_event("save", %{"user" => user_params}, socket) do + case Accounts.apply_user_email(socket.assigns.current_user, user_params) do + {:ok, applied_user} -> + Accounts.deliver_update_email_instructions( + applied_user, + socket.assigns.current_user.email, + &Routes.user_settings_url(socket, :confirm_email, &1) + ) + + send(self(), {:clear_flash_message, :error}) + + send( + self(), + {:put_flash_message, + {:info, "A link to confirm your e-mail change has been sent to the new address."}} + ) + + socket + |> assign(changeset: get_changeset(socket.assigns.current_user)) + |> live_noreply() + + {:error, cs} -> + cu = socket.assigns.current_user + cpw = user_params["current_password"] + valid_password? = User.valid_password?(cu, cpw) + msg = (valid_password? && "Could not reset email.") || "Invalid current password." + send(self(), {:put_flash_message, {:error, msg}}) + + socket + |> assign(changeset: cs) + |> live_noreply() + end + end +end diff --git a/lib/bones73k_web/live/user/settings/email.html.leex b/lib/bones73k_web/live/user/settings/email.html.leex new file mode 100644 index 00000000..b589b26e --- /dev/null +++ b/lib/bones73k_web/live/user/settings/email.html.leex @@ -0,0 +1,51 @@ +
+ +

Change email

+ + <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %> + +
+ <%= label f, :email, class: "form-label" %> +
+ + <%= icon_div @socket, "bi-at", [class: "icon fs-5"] %> + + <%= email_input f, :email, + value: input_value(f, :email), + class: input_class(f, :email, "form-control"), + placeholder: "e.g., babka@73k.us", + maxlength: User.max_email, + phx_debounce: "600", + aria_describedby: error_id(f, :email) + %> + <%= error_tag f, :email %> +
+
+ + +
+ <%= label f, :current_password, class: "form-label" %> +
+ + <%= icon_div @socket, "bi-lock", [class: "icon fs-5"] %> + + <%= password_input f, :current_password, + class: "form-control", + required: true, + aria_describedby: error_id(f, :current_password) + %> + <%= error_tag f, :current_password %> +
+
+ +
+ <%= submit "Change email", + disabled: !@changeset.valid? || input_value(f, :current_password) == "", + class: "btn btn-primary", + phx_disable_with: "Saving..." + %> +
+ + <% end %> + +
diff --git a/lib/bones73k_web/live/user/settings/password.ex b/lib/bones73k_web/live/user/settings/password.ex new file mode 100644 index 00000000..2225ff9f --- /dev/null +++ b/lib/bones73k_web/live/user/settings/password.ex @@ -0,0 +1,57 @@ +defmodule Bones73kWeb.UserLive.Settings.Password do + use Bones73kWeb, :live_component + + alias Bones73k.Accounts + alias Bones73k.Accounts.User + + @impl true + def update(%{current_user: user} = assigns, socket) do + socket + |> assign(id: assigns.id) + |> assign(current_user: user) + |> assign(changeset: get_changeset(user)) + |> assign(login_params: init_login_params(socket)) + |> assign(trigger_submit: false) + |> live_okreply() + end + + defp get_changeset(user, user_params \\ %{}) do + Accounts.change_user_password(user, user_params) + end + + defp init_login_params(socket) do + %{ + user_id: nil, + user_return_to: Routes.user_settings_path(socket, :edit), + messages: [info: "Password updated successfully."] + } + end + + @impl true + def handle_event("validate", %{"user" => user_params}, socket) do + cs = get_changeset(socket.assigns.current_user, user_params) + {:noreply, assign(socket, changeset: %{cs | action: :update})} + end + + @impl true + def handle_event("save", %{"user" => user_params}, socket) do + case Accounts.update_user_password(socket.assigns.current_user, user_params) do + {:ok, user} -> + socket + |> assign(login_params: %{socket.assigns.login_params | user_id: user.id}) + |> assign(trigger_submit: true) + |> live_noreply() + + {:error, cs} -> + cu = socket.assigns.current_user + cpw = user_params["current_password"] + valid_password? = User.valid_password?(cu, cpw) + msg = (valid_password? && "Could not change password.") || "Invalid current password." + send(self(), {:put_flash_message, {:error, msg}}) + + socket + |> assign(changeset: cs) + |> live_noreply() + end + end +end diff --git a/lib/bones73k_web/live/user/settings/password.html.leex b/lib/bones73k_web/live/user/settings/password.html.leex new file mode 100644 index 00000000..9d84ad4d --- /dev/null +++ b/lib/bones73k_web/live/user/settings/password.html.leex @@ -0,0 +1,76 @@ +
+ +

Change password

+ + <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %> + +
+ <%= label f, :password, "New password", class: "form-label" %> +
+ + <%= icon_div @socket, "bi-key", [class: "icon fs-5"] %> + + <%= password_input f, :password, + value: input_value(f, :password), + class: input_class(f, :password, "form-control"), + maxlength: User.max_password, + phx_debounce: "600", + aria_describedby: error_id(f, :password) + %> + <%= error_tag f, :password %> +
+
+ +
+ <%= label f, :password_confirmation, "Confirm new password", class: "form-label" %> +
+ + <%= icon_div @socket, "bi-key-fill", [class: "icon fs-5"] %> + + <%= password_input f, :password_confirmation, + value: input_value(f, :password_confirmation), + class: input_class(f, :password_confirmation, "form-control"), + maxlength: User.max_password, + aria_describedby: error_id(f, :password_confirmation) + %> + <%= error_tag f, :password_confirmation %> +
+
+ +
+ <%= label f, :current_password, class: "form-label" %> +
+ + <%= icon_div @socket, "bi-lock", [class: "icon fs-5"] %> + + <%= password_input f, :current_password, + class: "form-control", + required: true, + aria_describedby: error_id(f, :current_password) + %> + <%= error_tag f, :current_password %> +
+
+ +
+ <%= submit "Change password", + disabled: !@changeset.valid? || input_value(f, :current_password) == "", + class: "btn btn-primary", + phx_disable_with: "Saving..." + %> +
+ + <% end %> + + <%# hidden form for initial login after registration %> + <%= form_for :user, Routes.user_session_path(@socket, :create), [phx_trigger_action: @trigger_submit, id: "settings_pw_change_trigger"], fn f -> %> + <%= hidden_input f, :params_token, value: Phoenix.Token.encrypt(Bones73kWeb.Endpoint, "login_params", @login_params) %> + <% end %> + + <%# hidden form to submit user for relogin after password change %> + <%#= form_for :user_login, Routes.user_session_path(@socket, :create), [phx_trigger_action: @trigger_submit], fn f -> %> + <%#= hidden_input f, :login_params_token, value: Phoenix.Token.encrypt(Bones73kWeb.Endpoint, "login_params", @login_params) %> + <%#= hidden_input f, :remember_me, value: false %> + <%# end %> + +
diff --git a/lib/bones73k_web/router.ex b/lib/bones73k_web/router.ex index b7d3cfc8..3907c3ce 100644 --- a/lib/bones73k_web/router.ex +++ b/lib/bones73k_web/router.ex @@ -72,13 +72,9 @@ defmodule Bones73kWeb.Router do pipe_through([:browser, :require_authenticated_user]) # # liveview user settings - # live "/users/settings", UserLive.Settings, :edit + live "/users/settings", UserLive.Settings, :edit # original user routes from phx.gen.auth - # TODO: - get("/users/settings", UserSettingsController, :edit) - put("/users/settings/update_password", UserSettingsController, :update_password) - put("/users/settings/update_email", UserSettingsController, :update_email) get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email) end @@ -86,7 +82,6 @@ defmodule Bones73kWeb.Router do pipe_through([:browser]) delete("/users/log_out", UserSessionController, :delete) - # TODO: understanding/testing force_logout? get("/users/force_logout", UserSessionController, :force_logout) get("/users/confirm", UserConfirmationController, :new) post("/users/confirm", UserConfirmationController, :create) diff --git a/lib/bones73k_web/templates/layout/app.html.eex b/lib/bones73k_web/templates/layout/app.html.eex index dca8b5bd..c28896f7 100644 --- a/lib/bones73k_web/templates/layout/app.html.eex +++ b/lib/bones73k_web/templates/layout/app.html.eex @@ -2,7 +2,7 @@ <%# phoenix flash alerts: %>
-
+
<%= for {kind, color} <- alert_kinds() do %> <%= if flash_content = get_flash(@conn, kind) do %>