diff --git a/assets/js/_hamburger-helper.js b/assets/js/_hamburger-helper.js index a03c992c..c9c4f479 100644 --- a/assets/js/_hamburger-helper.js +++ b/assets/js/_hamburger-helper.js @@ -1,12 +1,14 @@ const togglerBtn = document.getElementById('navbarSupportedContentToggler'); const navbarContent = document.getElementById('navbarSupportedContent'); -navbarContent.addEventListener('show.bs.collapse', () => { - console.log('opening navbar content'); - togglerBtn.classList.toggle('is-active'); -}); - -navbarContent.addEventListener('hide.bs.collapse', () => { - console.log('closing navbar content'); - togglerBtn.classList.toggle('is-active'); -}); +if (navbarContent != null) { + navbarContent.addEventListener('show.bs.collapse', () => { + console.log('opening navbar content'); + togglerBtn.classList.toggle('is-active'); + }); + + navbarContent.addEventListener('hide.bs.collapse', () => { + console.log('closing navbar content'); + togglerBtn.classList.toggle('is-active'); + }); +} diff --git a/config/config.exs b/config/config.exs index ad34667d..5d43f0c0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -16,7 +16,8 @@ config :shift73k, config :shift73k, :app_global_vars, time_zone: "America/New_York", mailer_reply_to: "reply_to@example.com", - mailer_from: "app_name@example.com" + mailer_from: "app_name@example.com", + allow_registration: :true # Configures the endpoint config :shift73k, Shift73kWeb.Endpoint, diff --git a/lib/shift73k/accounts.ex b/lib/shift73k/accounts.ex index 199a52b3..1ca0eeef 100644 --- a/lib/shift73k/accounts.ex +++ b/lib/shift73k/accounts.ex @@ -108,6 +108,13 @@ defmodule Shift73k.Accounts do """ def register_user(attrs) do + # If attrs has atom keys, convert to string + # If attrs don't include role, put default role + attrs = + attrs + |> Map.new(fn {k, v} -> {to_string(k), v} end) + |> Map.put_new("role", registration_role()) + %User{} |> User.registration_changeset(attrs) |> Repo.insert() diff --git a/lib/shift73k_web/live/user/registration.ex b/lib/shift73k_web/live/user/registration.ex index 8b86b0fe..fc0436a3 100644 --- a/lib/shift73k_web/live/user/registration.ex +++ b/lib/shift73k_web/live/user/registration.ex @@ -1,6 +1,6 @@ defmodule Shift73kWeb.UserLive.Registration do use Shift73kWeb, :live_view - + alias Shift73k.Repo alias Shift73k.Accounts alias Shift73k.Accounts.User @@ -20,9 +20,7 @@ defmodule Shift73kWeb.UserLive.Registration do user_id: nil, user_return_to: Map.get(session, "user_return_to", "/"), 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." + success: "Welcome! Your new account has been created, and you've been logged in." ] } end @@ -35,19 +33,33 @@ defmodule Shift73kWeb.UserLive.Registration do @impl true def handle_event("save", %{"user" => user_params}, socket) do + is_first_user = !Repo.exists?(User) user_params - |> Map.put("role", Accounts.registration_role()) |> Accounts.register_user() |> case do {:ok, user} -> - {:ok, _, %Swoosh.Email{} = _captured_email} = - Accounts.deliver_user_confirmation_instructions( - user, - &Routes.user_confirmation_url(socket, :confirm, &1) - ) + # If this is the first user, we just confirm them + if is_first_user do + user |> User.confirm_changeset() |> Repo.update() + else + # Otherwise, all new users require email confirmation so we wend instructions + {:ok, _, %Swoosh.Email{} = _captured_email} = + Accounts.deliver_user_confirmation_instructions( + user, + &Routes.user_confirmation_url(socket, :confirm, &1) + ) + end + + login_params = + if is_first_user do + socket.assigns.login_params + else + put_in(socket.assigns.login_params, [:messages, :info], "Some features may be unavailable until you confirm your email address. Check your inbox for instructions.") + end + |> put_in([:user_id], user.id) socket - |> assign(login_params: %{socket.assigns.login_params | user_id: user.id}) + |> assign(login_params: login_params) |> assign(trigger_submit: true) |> live_noreply() diff --git a/lib/shift73k_web/live/user/registration.html.heex b/lib/shift73k_web/live/user/registration.html.heex index 41f9a11d..ca00d39c 100644 --- a/lib/shift73k_web/live/user/registration.html.heex +++ b/lib/shift73k_web/live/user/registration.html.heex @@ -6,7 +6,7 @@

Create an account to manage your work shifts with us.

- <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "reg_form"], fn f -> %> + <.form let={f} for={@changeset} phx-change="validate" phx-submit="save" novalidate id="reg_form"> <%= label f, :email, class: "form-label" %>
@@ -45,7 +45,7 @@ %>
- <% end %> +

<%= link "Log in", to: Routes.user_session_path(@socket, :new) %> | diff --git a/lib/shift73k_web/live/user/reset_password.ex b/lib/shift73k_web/live/user/reset_password.ex index 2c2b801c..2aa5856c 100644 --- a/lib/shift73k_web/live/user/reset_password.ex +++ b/lib/shift73k_web/live/user/reset_password.ex @@ -4,6 +4,9 @@ defmodule Shift73kWeb.UserLive.ResetPassword do alias Shift73k.Accounts alias Shift73k.Accounts.User + @app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true) + @app_allow_registration @app_vars[:allow_registration] + @impl true def mount(_params, session, socket) do user = Accounts.get_user!(session["user_id"]) @@ -37,4 +40,6 @@ defmodule Shift73kWeb.UserLive.ResetPassword do |> assign(changeset: changeset)} end end + + def allow_registration, do: @app_allow_registration end diff --git a/lib/shift73k_web/live/user/reset_password.html.heex b/lib/shift73k_web/live/user/reset_password.html.heex index f7761c69..0abfcea5 100644 --- a/lib/shift73k_web/live/user/reset_password.html.heex +++ b/lib/shift73k_web/live/user/reset_password.html.heex @@ -45,7 +45,9 @@ <% end %>

- <%= link "Register", to: Routes.user_registration_path(@socket, :new) %> | + <%= if allow_registration() do %> + <%= link "Register", to: Routes.user_registration_path(@socket, :new) %> | + <% end %> <%= link "Log in", to: Routes.user_session_path(@socket, :new) %>

diff --git a/lib/shift73k_web/plugs/ensure_allow_registration_plug.ex b/lib/shift73k_web/plugs/ensure_allow_registration_plug.ex new file mode 100644 index 00000000..7dfa0e96 --- /dev/null +++ b/lib/shift73k_web/plugs/ensure_allow_registration_plug.ex @@ -0,0 +1,35 @@ +defmodule Shift73kWeb.EnsureAllowRegistrationPlug do + @moduledoc """ + This plug ensures that there is at least one known User. + """ + import Plug.Conn + import Phoenix.Controller + + alias Shift73k.Repo + alias Shift73k.Accounts.User + + @app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true) + @app_allow_registration @app_vars[:allow_registration] + + @doc false + @spec init(any()) :: any() + def init(config), do: config + + @doc false + @spec call(Conn.t(), atom() | [atom()]) :: Conn.t() + def call(conn, _opts) do + # If there aren't even any users, or registration is allowed + if !Repo.exists?(User) || @app_allow_registration do + # We will allow registration + conn + else + # Otherwise, + # if app is configured to not allow registration, + # and there is a user, + # then we redirect to root URL + conn + |> redirect(to: "/") + |> halt() + end + end +end diff --git a/lib/shift73k_web/plugs/ensure_role_plug.ex b/lib/shift73k_web/plugs/ensure_role_plug.ex index 9c604e11..e8ac84d5 100644 --- a/lib/shift73k_web/plugs/ensure_role_plug.ex +++ b/lib/shift73k_web/plugs/ensure_role_plug.ex @@ -27,8 +27,7 @@ defmodule Shift73kWeb.EnsureRolePlug do def call(conn, roles) do user_token = get_session(conn, :user_token) - (user_token && - Accounts.get_user_by_session_token(user_token)) + (user_token && Accounts.get_user_by_session_token(user_token)) |> has_role?(roles) |> maybe_halt(conn) end diff --git a/lib/shift73k_web/plugs/ensure_user_exist_plug.ex b/lib/shift73k_web/plugs/ensure_user_exist_plug.ex new file mode 100644 index 00000000..3856fec9 --- /dev/null +++ b/lib/shift73k_web/plugs/ensure_user_exist_plug.ex @@ -0,0 +1,30 @@ +defmodule Shift73kWeb.EnsureUserExistPlug do + @moduledoc """ + This plug ensures that there is at least one known User. + """ + import Plug.Conn + import Phoenix.Controller + + alias Shift73k.Repo + alias Shift73k.Accounts.User + alias Shift73kWeb.Router.Helpers, as: Routes + + @doc false + @spec init(any()) :: any() + def init(config), do: config + + @doc false + @spec call(Conn.t(), atom() | [atom()]) :: Conn.t() + def call(conn, _opts) do + # If there aren't even any users, + if !Repo.exists?(User) do + # We're just going to redirect to registration + conn + |> redirect(to: Routes.user_registration_path(conn, :new)) + |> halt() + else + # Otherwise we proceed as normal + conn + end + end +end diff --git a/lib/shift73k_web/router.ex b/lib/shift73k_web/router.ex index b45acda4..7ed69670 100644 --- a/lib/shift73k_web/router.ex +++ b/lib/shift73k_web/router.ex @@ -2,27 +2,37 @@ defmodule Shift73kWeb.Router do use Shift73kWeb, :router import Shift73kWeb.UserAuth alias Shift73kWeb.EnsureRolePlug + alias Shift73kWeb.EnsureUserExistPlug + alias Shift73kWeb.EnsureAllowRegistrationPlug pipeline :browser do - plug(:accepts, ["html"]) - plug(:fetch_session) - plug(:fetch_live_flash) - plug(:put_root_layout, {Shift73kWeb.LayoutView, :root}) - plug(:protect_from_forgery) - plug(:put_secure_browser_headers) - plug(:fetch_current_user) + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, {Shift73kWeb.LayoutView, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + plug :fetch_current_user end - pipeline :user do - plug(EnsureRolePlug, [:admin, :manager, :user]) + pipeline :ensure_role_user do + plug EnsureRolePlug, [:admin, :manager, :user] end - pipeline :manager do - plug(EnsureRolePlug, [:admin, :manager]) + pipeline :ensure_user_exist do + plug EnsureUserExistPlug end - pipeline :admin do - plug(EnsureRolePlug, :admin) + pipeline :ensure_allow_registration do + plug EnsureAllowRegistrationPlug + end + + pipeline :ensure_role_manager do + plug EnsureRolePlug, [:admin, :manager] + end + + pipeline :ensure_role_admin do + plug EnsureRolePlug, :admin end # Enables the Swoosh mailbox preview in development. @@ -38,49 +48,54 @@ defmodule Shift73kWeb.Router do end scope "/", Shift73kWeb do - pipe_through([:browser]) + pipe_through([:browser, :ensure_user_exist]) - get("/", Redirector, to: "/assign") + get "/", Redirector, to: "/assign" end scope "/", Shift73kWeb do - pipe_through([:browser, :redirect_if_user_is_authenticated]) + pipe_through [:browser, :redirect_if_user_is_authenticated, :ensure_allow_registration] + + get "/users/register", UserRegistrationController, :new + end + + scope "/", Shift73kWeb do + pipe_through [:browser, :redirect_if_user_is_authenticated, :ensure_user_exist] # session routes, irrelevant if user is authenticated - get("/users/register", UserRegistrationController, :new) - get("/users/log_in", UserSessionController, :new) - post("/users/log_in", UserSessionController, :create) - get("/users/reset_password", UserResetPasswordController, :new) - post("/users/reset_password", UserResetPasswordController, :create) - get("/users/reset_password/:token", UserResetPasswordController, :edit) + get "/users/log_in", UserSessionController, :new + post "/users/log_in", UserSessionController, :create + get "/users/reset_password", UserResetPasswordController, :new + post "/users/reset_password", UserResetPasswordController, :create + get "/users/reset_password/:token", UserResetPasswordController, :edit end scope "/", Shift73kWeb do - pipe_through([:browser, :require_authenticated_user]) + pipe_through [:browser, :require_authenticated_user] # user settings (change email, password, calendar week start, etc) - live("/users/settings", UserLive.Settings, :edit) + live "/users/settings", UserLive.Settings, :edit # confirm email by token - get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email) + get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email end scope "/", Shift73kWeb do - pipe_through([:browser]) + pipe_through [:browser, :ensure_user_exist] # session paths - delete("/users/log_out", UserSessionController, :delete) - get("/users/force_logout", UserSessionController, :force_logout) - get("/users/confirm", UserConfirmationController, :new) - post("/users/confirm", UserConfirmationController, :create) - get("/users/confirm/:token", UserConfirmationController, :confirm) + delete "/users/log_out", UserSessionController, :delete + get "/users/force_logout", UserSessionController, :force_logout + get "/users/confirm", UserConfirmationController, :new + post "/users/confirm", UserConfirmationController, :create + get "/users/confirm/:token", UserConfirmationController, :confirm # ics/ical route for user's shifts - get("/ics/:slug", UserShiftsIcsController, :index) + get "/ics/:slug", UserShiftsIcsController, :index end scope "/", Shift73kWeb do - pipe_through([:browser, :require_authenticated_user, :user]) + pipe_through [:browser, :require_authenticated_user, :ensure_role_user] live "/templates", ShiftTemplateLive.Index, :index live "/templates/new", ShiftTemplateLive.Index, :new @@ -98,16 +113,16 @@ defmodule Shift73kWeb.Router do end # scope "/", Shift73kWeb do - # pipe_through([:browser, :require_authenticated_user, :admin]) + # pipe_through([:browser, :require_authenticated_user, :ensure_role_admin]) # end # Users Management scope "/users", Shift73kWeb do - pipe_through([:browser, :require_authenticated_user, :manager, :require_email_confirmed]) + pipe_through [:browser, :require_authenticated_user, :ensure_role_manager, :require_email_confirmed] - live("/", UserManagementLive.Index, :index) - live("/new", UserManagementLive.Index, :new) - live("/edit/:id", UserManagementLive.Index, :edit) + live "/", UserManagementLive.Index, :index + live "/new", UserManagementLive.Index, :new + live "/edit/:id", UserManagementLive.Index, :edit # resources "/", UserManagementController, only: [:new, :create, :edit, :update] end end diff --git a/lib/shift73k_web/templates/layout/_navbar.html.heex b/lib/shift73k_web/templates/layout/_navbar.html.heex index a98856e6..4c81942d 100644 --- a/lib/shift73k_web/templates/layout/_navbar.html.heex +++ b/lib/shift73k_web/templates/layout/_navbar.html.heex @@ -8,79 +8,63 @@ <% end %> - <%= if @current_user do %> + <%# If there's a current user, + OR if there are users & we allow registration, + THEN we will show a full menu configuration %> + + <%= if @current_user || (Repo.exists?(User) && allow_registration()) do %> + - <% else %> - <%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-block d-lg-none") do %> - Log in - <% end %> - <% end %> - diff --git a/lib/shift73k_web/templates/layout/navbar/_nouser_menu.html.heex b/lib/shift73k_web/templates/layout/navbar/_nouser_menu.html.heex new file mode 100644 index 00000000..59203835 --- /dev/null +++ b/lib/shift73k_web/templates/layout/navbar/_nouser_menu.html.heex @@ -0,0 +1,23 @@ + diff --git a/lib/shift73k_web/templates/layout/navbar/_user_menu.html.heex b/lib/shift73k_web/templates/layout/navbar/_user_menu.html.heex index c74a7324..191b291b 100644 --- a/lib/shift73k_web/templates/layout/navbar/_user_menu.html.heex +++ b/lib/shift73k_web/templates/layout/navbar/_user_menu.html.heex @@ -1,7 +1,7 @@