diff --git a/lib/bones73k_web/controllers/user_registration_controller.ex b/lib/bones73k_web/controllers/user_registration_controller.ex index 58e9c83..108915c 100644 --- a/lib/bones73k_web/controllers/user_registration_controller.ex +++ b/lib/bones73k_web/controllers/user_registration_controller.ex @@ -1,33 +1,8 @@ defmodule Bones73kWeb.UserRegistrationController do use Bones73kWeb, :controller - - alias Bones73k.Accounts - alias Bones73k.Accounts.User - alias Bones73kWeb.UserAuth + import Phoenix.LiveView.Controller def new(conn, _params) do - changeset = Accounts.change_user_registration(%User{}, %{role: Accounts.registration_role()}) - render(conn, "new.html", changeset: changeset) - end - - def create(conn, %{"user" => user_params}) do - user_params - |> Map.put_new("role", Accounts.registration_role()) - |> Accounts.register_user() - |> case do - {:ok, user} -> - %Bamboo.Email{} = - Accounts.deliver_user_confirmation_instructions( - user, - &Routes.user_confirmation_url(conn, :confirm, &1) - ) - - conn - |> put_flash(:info, "User created successfully.") - |> UserAuth.log_in_user(user) - - {:error, %Ecto.Changeset{} = changeset} -> - render(conn, "new.html", changeset: changeset) - end + live_render(conn, Bones73kWeb.UserLive.Registration) end end diff --git a/lib/bones73k_web/controllers/user_session_controller.ex b/lib/bones73k_web/controllers/user_session_controller.ex index 5c9f68d..fe5f61f 100644 --- a/lib/bones73k_web/controllers/user_session_controller.ex +++ b/lib/bones73k_web/controllers/user_session_controller.ex @@ -2,15 +2,15 @@ defmodule Bones73kWeb.UserSessionController do use Bones73kWeb, :controller alias Bones73k.Accounts + alias Bones73k.Accounts.User alias Bones73kWeb.UserAuth def new(conn, _params) do + # IO.inspect(conn.private, label: "session_new conn.private :") render(conn, "new.html", error_message: nil) end - def create(conn, %{"user" => user_params}) do - %{"email" => email, "password" => password} = user_params - + 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) else @@ -18,6 +18,22 @@ defmodule Bones73kWeb.UserSessionController do end end + def create(conn, %{"user" => %{"params_token" => token} = user_params}) do + with {:ok, params} <- Phoenix.Token.decrypt(Bones73kWeb.Endpoint, "login_params", token), + %User{} = user <- Accounts.get_user(params.user_id) do + conn + |> collect_messages(params.messages) + |> put_session(:user_return_to, params.user_return_to) + |> UserAuth.log_in_user(user, Map.put_new(user_params, "remember_me", "false")) + else + _ -> render(conn, "new.html", error_message: "Invalid email or password") + end + end + + defp collect_messages(conn, messages) do + Enum.reduce(messages, conn, fn {type, msg}, acc -> put_flash(acc, type, msg) end) + end + def delete(conn, _params) do conn |> put_flash(:info, "Logged out successfully.") diff --git a/lib/bones73k_web/live/admin_dashboard_live.ex b/lib/bones73k_web/live/admin_dashboard_live.ex index 9010799..21eb440 100644 --- a/lib/bones73k_web/live/admin_dashboard_live.ex +++ b/lib/bones73k_web/live/admin_dashboard_live.ex @@ -3,7 +3,7 @@ defmodule Bones73kWeb.AdminDashboardLive do @impl true def mount(_params, session, socket) do - socket = assign_defaults(session, socket) + socket = assign_defaults(socket, session) {:ok, socket} end diff --git a/lib/bones73k_web/live/live_helpers.ex b/lib/bones73k_web/live/live_helpers.ex index 01cf17a..ee5fb91 100644 --- a/lib/bones73k_web/live/live_helpers.ex +++ b/lib/bones73k_web/live/live_helpers.ex @@ -2,10 +2,21 @@ defmodule Bones73kWeb.LiveHelpers do import Phoenix.LiveView alias Bones73k.Accounts alias Bones73k.Accounts.User - alias Bones73kWeb.Router.Helpers, as: Routes alias Bones73kWeb.UserAuth import Phoenix.LiveView.Helpers + @doc """ + Performs the {:noreply, socket} for a given socket. + This helps make the noreply pipeable + """ + def live_noreply(socket), do: {:noreply, socket} + + @doc """ + Performs the {:ok, socket} for a given socket. + This helps make the ok reply pipeable + """ + def live_okreply(socket), do: {:ok, socket} + @doc """ Renders a component inside the `Bones73kWeb.ModalComponent` component. @@ -26,28 +37,21 @@ defmodule Bones73kWeb.LiveHelpers do live_component(socket, Bones73kWeb.ModalComponent, modal_opts) end - def assign_defaults(session, socket) do + @doc """ + Loads default assigns for liveviews + """ + def assign_defaults(socket, session) do Bones73kWeb.Endpoint.subscribe(UserAuth.pubsub_topic()) + assign_current_user(socket, session) + end - socket = - assign_new(socket, :current_user, fn -> - find_current_user(session) - end) - - case socket.assigns.current_user do - %User{} -> - socket - - _other -> - socket - |> put_flash(:error, "You must log in to access this page.") - |> redirect(to: Routes.user_session_path(socket, :new)) + # For liveviews, ensures current_user is in socket assigns. + def assign_current_user(socket, session) do + with user_token when not is_nil(user_token) <- session["user_token"], + %User{} = user <- Accounts.get_user_by_session_token(user_token) do + assign(socket, :current_user, user) + else + _ -> socket end end - - defp find_current_user(session) do - with user_token when not is_nil(user_token) <- session["user_token"], - %User{} = user <- Accounts.get_user_by_session_token(user_token), - do: user - end end diff --git a/lib/bones73k_web/live/page_live.ex b/lib/bones73k_web/live/page_live.ex index 59463e2..c694d50 100644 --- a/lib/bones73k_web/live/page_live.ex +++ b/lib/bones73k_web/live/page_live.ex @@ -3,7 +3,7 @@ defmodule Bones73kWeb.PageLive do @impl true def mount(_params, session, socket) do - socket = assign_defaults(session, socket) + socket = assign_defaults(socket, session) {:ok, assign(socket, query: "", results: %{})} end diff --git a/lib/bones73k_web/live/property_live/index.ex b/lib/bones73k_web/live/property_live/index.ex index 185083a..33c7b95 100644 --- a/lib/bones73k_web/live/property_live/index.ex +++ b/lib/bones73k_web/live/property_live/index.ex @@ -7,7 +7,7 @@ defmodule Bones73kWeb.PropertyLive.Index do @impl true def mount(_params, session, socket) do - socket = assign_defaults(session, socket) + socket = assign_defaults(socket, session) {:ok, assign(socket, :properties, [])} end diff --git a/lib/bones73k_web/live/property_live/show.ex b/lib/bones73k_web/live/property_live/show.ex index 7fc8c01..b611312 100644 --- a/lib/bones73k_web/live/property_live/show.ex +++ b/lib/bones73k_web/live/property_live/show.ex @@ -6,7 +6,7 @@ defmodule Bones73kWeb.PropertyLive.Show do @impl true def mount(_params, session, socket) do - socket = assign_defaults(session, socket) + socket = assign_defaults(socket, session) {:ok, socket} end diff --git a/lib/bones73k_web/live/user/registration.ex b/lib/bones73k_web/live/user/registration.ex new file mode 100644 index 0000000..6e990fc --- /dev/null +++ b/lib/bones73k_web/live/user/registration.ex @@ -0,0 +1,63 @@ +defmodule Bones73kWeb.UserLive.Registration do + use Bones73kWeb, :live_view + + 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 + |> assign_defaults(session) + |> assign(page_title: "Register") + |> assign(changeset: Accounts.change_user_registration(%User{})) + |> assign(login_params: init_login_params(session)) + |> assign(trigger_submit: false) + |> live_okreply() + end + + defp init_login_params(session) do + %{ + user_id: nil, + user_return_to: Map.get(session, "user_return_to", "/"), + messages: @messages + } + end + + @impl true + def handle_event("validate", %{"user" => user_params}, socket) do + cs = Accounts.change_user_registration(%User{}, user_params) + {:noreply, assign(socket, changeset: %{cs | action: :update})} + end + + @impl true + def handle_event("save", %{"user" => user_params}, socket) do + user_params + |> Map.put("role", Accounts.registration_role()) + |> Accounts.register_user() + |> case do + {:ok, user} -> + %Bamboo.Email{} = + Accounts.deliver_user_confirmation_instructions( + user, + &Routes.user_confirmation_url(socket, :confirm, &1) + ) + + socket + |> assign(login_params: %{socket.assigns.login_params | user_id: user.id}) + |> assign(trigger_submit: true) + |> live_noreply() + + {:error, cs} -> + socket + |> put_flash(:error, "Ope — registration failed for some reason.") + |> assign(:changeset, cs) + |> live_noreply() + end + end +end diff --git a/lib/bones73k_web/live/user/registration.html.leex b/lib/bones73k_web/live/user/registration.html.leex new file mode 100644 index 0000000..c2c7368 --- /dev/null +++ b/lib/bones73k_web/live/user/registration.html.leex @@ -0,0 +1,68 @@ +
+
+ +

+ <%= icon_div @socket, "bi-person-plus", [class: "icon baseline"] %> + Register +

+

Registration gains additional features, like remembering your song request history.

+ + <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "reg_form"], 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, + required: true, + autofocus: true, + phx_debounce: "blur", + aria_describedby: error_id(f, :email) + %> + <%= error_tag f, :email %> +
+
+ +
+ <%= label f, :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"), + minlength: User.min_password, + maxlength: User.max_password, + required: true, + phx_debounce: "200", + aria_describedby: error_id(f, :password) + %> + <%= error_tag f, :password %> +
+
+ +
+ <%= submit (@trigger_submit && "Saving..." || "Register"), disabled: @trigger_submit || !@changeset.valid?, class: "btn btn-primary", phx_disable_with: "Saving..." %> +
+ + <% end %> + +

+ <%= link "Log in", to: Routes.user_session_path(@socket, :new) %> | + <%= link "Forgot your password?", to: Routes.user_reset_password_path(@socket, :new) %> +

+ + <%# hidden form for initial login after registration %> + <%= form_for :user, Routes.user_session_path(@socket, :create), [phx_trigger_action: @trigger_submit, id: "reg_trigger"], fn f -> %> + <%= hidden_input f, :params_token, value: Phoenix.Token.encrypt(Bones73kWeb.Endpoint, "login_params", @login_params) %> + <% end %> + +
+
diff --git a/lib/bones73k_web/live/user_dashboard_live.ex b/lib/bones73k_web/live/user_dashboard_live.ex index 154e419..a6def82 100644 --- a/lib/bones73k_web/live/user_dashboard_live.ex +++ b/lib/bones73k_web/live/user_dashboard_live.ex @@ -3,7 +3,7 @@ defmodule Bones73kWeb.UserDashboardLive do @impl true def mount(_params, session, socket) do - socket = assign_defaults(session, socket) + socket = assign_defaults(socket, session) {:ok, socket} end diff --git a/lib/bones73k_web/router.ex b/lib/bones73k_web/router.ex index 6d349d7..e662ebe 100644 --- a/lib/bones73k_web/router.ex +++ b/lib/bones73k_web/router.ex @@ -18,13 +18,24 @@ defmodule Bones73kWeb.Router do end pipeline :user do - plug(EnsureRolePlug, [:admin, :user]) + plug(EnsureRolePlug, [:admin, :manager, :user]) + end + + pipeline :manager do + plug(EnsureRolePlug, [:admin, :manager]) end pipeline :admin do plug(EnsureRolePlug, :admin) end + scope "/", Bones73kWeb do + pipe_through [:browser] + + live "/", PageLive, :index + get "/other", OtherController, :index + end + # Other scopes may use custom stacks. # scope "/api", Bones73kWeb do # pipe_through :api @@ -46,15 +57,18 @@ defmodule Bones73kWeb.Router do end end - ## Authentication routes - scope "/", Bones73kWeb do pipe_through([:browser, :redirect_if_user_is_authenticated]) + # # liveview user auth routes + # live "/users/reset_password", UserLive.ResetPassword, :new + # live "/users/reset_password/:token", UserLive.ResetPassword, :edit + + # original user auth routes from phx.gen.auth get("/users/register", UserRegistrationController, :new) - post("/users/register", UserRegistrationController, :create) get("/users/log_in", UserSessionController, :new) post("/users/log_in", UserSessionController, :create) + # TODO: get("/users/reset_password", UserResetPasswordController, :new) post("/users/reset_password", UserResetPasswordController, :create) get("/users/reset_password/:token", UserResetPasswordController, :edit) @@ -64,26 +78,27 @@ defmodule Bones73kWeb.Router do scope "/", Bones73kWeb do pipe_through([:browser, :require_authenticated_user]) + # # liveview user settings + # 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) - - # This line was moved - live("/", PageLive, :index) end scope "/", Bones73kWeb do pipe_through([:browser]) - get("/users/force_logout", UserSessionController, :force_logout) delete("/users/log_out", UserSessionController, :delete) + # TODO: understanding/testing force_logout? + get("/users/force_logout", UserSessionController, :force_logout) + # TODO: get("/users/confirm", UserConfirmationController, :new) post("/users/confirm", UserConfirmationController, :create) get("/users/confirm/:token", UserConfirmationController, :confirm) - - # Special non-live page for testing only - get("/other", OtherController, :index) end scope "/", Bones73kWeb do diff --git a/lib/bones73k_web/templates/layout/app.html.eex b/lib/bones73k_web/templates/layout/app.html.eex index 260836e..dca8b5b 100644 --- a/lib/bones73k_web/templates/layout/app.html.eex +++ b/lib/bones73k_web/templates/layout/app.html.eex @@ -1,15 +1,17 @@
<%# phoenix flash alerts: %> -
- <%= for {kind, color} <- alert_kinds() do %> - <%= if flash_content = get_flash(@conn, kind) do %> - +
+
+ <%= for {kind, color} <- alert_kinds() do %> + <%= if flash_content = get_flash(@conn, kind) do %> + + <% end %> <% end %> - <% end %> +
<%= @inner_content %> diff --git a/lib/bones73k_web/templates/layout/live.html.leex b/lib/bones73k_web/templates/layout/live.html.leex index 09a9457..889fc6d 100644 --- a/lib/bones73k_web/templates/layout/live.html.leex +++ b/lib/bones73k_web/templates/layout/live.html.leex @@ -1,15 +1,17 @@
<%# liveview flash alerts: %> -
- <%= for {kind, color} <- alert_kinds() do %> - <%= if flash_content = live_flash(@flash, kind) do %> - +
+
+ <%= for {kind, color} <- alert_kinds() do %> + <%= if flash_content = live_flash(@flash, kind) do %> + + <% end %> <% end %> - <% end %> +
<%= @inner_content %> diff --git a/lib/bones73k_web/templates/layout/navbar/_user_menu.html.eex b/lib/bones73k_web/templates/layout/navbar/_user_menu.html.eex index 09a5eb4..6fa6b94 100644 --- a/lib/bones73k_web/templates/layout/navbar/_user_menu.html.eex +++ b/lib/bones73k_web/templates/layout/navbar/_user_menu.html.eex @@ -1,7 +1,8 @@