implemented optional 'allow_registration' config, with first registered user being pre-confirmed Admin, registration unavailable after that point if allow_registration: :false

This commit is contained in:
Adam Piontek 2022-08-14 09:14:42 -04:00
parent ea74a89078
commit f27df8d676
22 changed files with 395 additions and 244 deletions

View file

@ -1,6 +1,7 @@
const togglerBtn = document.getElementById('navbarSupportedContentToggler');
const navbarContent = document.getElementById('navbarSupportedContent');
if (navbarContent != null) {
navbarContent.addEventListener('show.bs.collapse', () => {
console.log('opening navbar content');
togglerBtn.classList.toggle('is-active');
@ -10,3 +11,4 @@ navbarContent.addEventListener('hide.bs.collapse', () => {
console.log('closing navbar content');
togglerBtn.classList.toggle('is-active');
});
}

View file

@ -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,

View file

@ -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()

View file

@ -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} ->
# 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()

View file

@ -6,7 +6,7 @@
</h2>
<p class="lead">Create an account to manage your work shifts with us.</p>
<%= 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" %>
<div class="inner-addon left-addon mb-3" phx-feedback-for={input_id(f, :email)}>
@ -45,7 +45,7 @@
%>
</div>
<% end %>
</.form>
<p>
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %> |

View file

@ -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

View file

@ -45,7 +45,9 @@
<% end %>
<p class="mt-3 is-pulled-right">
<%= if allow_registration() do %>
<%= link "Register", to: Routes.user_registration_path(@socket, :new) %> |
<% end %>
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %>
</p>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -8,59 +8,26 @@
<% end %>
</h1>
<%= 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 %>
<button class="hamburger hamburger--squeeze collapsed navbar-toggler" id="navbarSupportedContentToggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="hamburger-box d-flex">
<span class="hamburger-inner"></span>
</span>
</button>
<% else %>
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-block d-lg-none") do %>
<i class="bi bi-door-open me-1"></i> Log in
<% end %>
<% end %>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<%# nav LEFT items %>
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<%#= if @current_user do %>
<%# <li class="nav-item"> %>
<%#= link nav_link_opts(@conn, to: Routes.shift_template_index_path(@conn, :index), class: "nav-link") do %>
<%#= icon_div @conn, "bi-clock-history", [class: "icon baseline me-1"] %>
<%# Templates %>
<%# end %>
<%# </li> %>
<%# end %>
<%# normal navbar link example %>
<%# <li class="nav-item"> %>
<%#= link "Properties", nav_link_opts(@conn, to: Routes.property_index_path(@conn, :index), class: "nav-link") %>
<%# </li> %>
<%# ACTIVE page link example %>
<%# <li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li> %>
<%# DISABLED page link example %>
<%# <li class="nav-item">
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
</li> %>
<%# normal dropdown menu example %>
<%# <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownExample" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownExample">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li> %>
</ul>
<%# nav RIGHT items %>
<ul class="navbar-nav">
@ -73,14 +40,31 @@
<% else %>
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light d-none d-lg-block") do %>
<i class="bi bi-door-open me-1"></i> Log in
<% end %>
<%= render "navbar/_nouser_menu.html", assigns %>
<% end %>
</ul>
</div>
<%# If there's no current user,
AND:
There are no users -- [REGISTER]
OR no registration allowed -- [LOG IN] %>
<%= else %>
<%= if !Repo.exists?(User) || allow_registration() do %>
<%= link nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "btn btn-outline-light") do %>
<i class="bi bi-journal-plus"></i> Register
<% end %>
<% else %>
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light") do %>
<i class="bi bi-door-open"></i> Log in
<% end %>
<% end %>
<% end %>
</div>
</nav>

View file

@ -0,0 +1,23 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle me-1"></i> Hello?
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
<li>
<%= link nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "dropdown-item") do %>
<i class="bi bi-journal-plus me-1"></i> Register
<% end %>
</li>
<li>
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "dropdown-item") do %>
<i class="bi bi-door-open me-1"></i> Log in
<% end %>
</li>
</ul>
</li>

View file

@ -1,7 +1,7 @@
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle me-1"></i> <%= @current_user && "Hello!" || "Hello?" %>
<i class="bi bi-person-circle me-1"></i> Hello!
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">

View file

@ -29,6 +29,9 @@
<% end %>
<p>
<%= if allow_registration() do %>
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<% end %>
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
</p>

View file

@ -27,7 +27,9 @@
<% end %>
<p>
<%= if allow_registration() do %>
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<% end %>
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
</p>

View file

@ -49,7 +49,9 @@
<% end %>
<p>
<%= if allow_registration() do %>
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<% end %>
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
</p>

View file

@ -1,9 +1,12 @@
defmodule Shift73kWeb.LayoutView do
use Shift73kWeb, :view
alias Shift73k.Repo
alias Shift73k.Accounts.User
alias Shift73kWeb.Roles
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
@app_allow_registration @app_vars[:allow_registration]
# With a Vite.js-based workflow, we will import different asset files in development
# and in production builds. Therefore, we will need a way to conditionally render
# <script> tags based on Mix environment. However, since Mix is not available in
@ -11,6 +14,8 @@ defmodule Shift73kWeb.LayoutView do
@env Mix.env() # remember value at compile time
def dev_env?, do: @env == :dev
def allow_registration, do: @app_allow_registration
def nav_link_opts(conn, opts) do
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
false -> opts

View file

@ -1,4 +1,9 @@
defmodule Shift73kWeb.UserConfirmationView do
use Shift73kWeb, :view
alias Shift73k.Accounts.User
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
@app_allow_registration @app_vars[:allow_registration]
def allow_registration, do: @app_allow_registration
end

View file

@ -1,4 +1,9 @@
defmodule Shift73kWeb.UserResetPasswordView do
use Shift73kWeb, :view
alias Shift73k.Accounts.User
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
@app_allow_registration @app_vars[:allow_registration]
def allow_registration, do: @app_allow_registration
end

View file

@ -1,4 +1,9 @@
defmodule Shift73kWeb.UserSessionView do
use Shift73kWeb, :view
alias Shift73k.Accounts.User
@app_vars Application.compile_env(:shift73k, :app_global_vars, allow_registration: :true)
@app_allow_registration @app_vars[:allow_registration]
def allow_registration, do: @app_allow_registration
end

View file

@ -14,6 +14,11 @@ alias Shift73k.Repo
alias Shift73k.Accounts
alias Shift73k.Accounts.User
if Mix.env() == :dev do
if System.get_env("ECTO_SEED_DB") do
############################################################################
## INSERTING MOCK USER DATA
@ -152,3 +157,7 @@ for user <- Accounts.list_users() do
|> Shifts.create_multiple()
end
end
end
end