made changes to how roles are set on registering new users

This commit is contained in:
Adam Piontek 2021-02-27 08:17:30 -05:00
parent 2358b6adfb
commit 42b2ea9d78
5 changed files with 135 additions and 62 deletions

View file

@ -10,6 +10,19 @@ defmodule Bones73k.Accounts do
## Database getters ## Database getters
@doc """
Returns the list of users.
## Examples
iex> list_users()
[%User{}, ...]
"""
def list_users do
Repo.all(User)
end
@doc """ @doc """
Gets a user by email. Gets a user by email.
@ -60,6 +73,22 @@ defmodule Bones73k.Accounts do
""" """
def get_user!(id), do: Repo.get!(User, id) def get_user!(id), do: Repo.get!(User, id)
@doc """
Gets a single user.
Returns nil if the User does not exist.
## Examples
iex> get_user(123)
%User{}
iex> get_user(456)
nil
"""
def get_user(id), do: Repo.get(User, id)
## User registration ## User registration
@doc """ @doc """
@ -81,22 +110,11 @@ defmodule Bones73k.Accounts do
end end
@doc """ @doc """
Registers an admin. Returns role for user registration. First user to register gets :admin role.
Following users get :user role. Used by registration controller/liveview.
## Examples Admins can choose role for new users they create, and modify other uses.
iex> register_admin(%{field: value})
{:ok, %User{}}
iex> register_admin(%{field: bad_value})
{:error, %Ecto.Changeset{}}
""" """
def register_admin(attrs) do def registration_role, do: (Repo.exists?(User) && :user) || :admin
%User{}
|> User.admin_registration_changeset(attrs)
|> Repo.insert()
end
def logout_user(%User{} = user) do def logout_user(%User{} = user) do
# Delete all user tokens # Delete all user tokens

View file

@ -5,9 +5,14 @@ defmodule Bones73k.Accounts.User do
defenum(RolesEnum, :role, [ defenum(RolesEnum, :role, [
:user, :user,
:manager,
:admin :admin
]) ])
@max_email 254
@min_password 6
@max_password 80
@derive {Inspect, except: [:password]} @derive {Inspect, except: [:password]}
schema "users" do schema "users" do
field :email, :string field :email, :string
@ -19,6 +24,10 @@ defmodule Bones73k.Accounts.User do
timestamps() timestamps()
end end
def max_email, do: @max_email
def min_password, do: @min_password
def max_password, do: @max_password
@doc """ @doc """
A user changeset for registration. A user changeset for registration.
@ -26,53 +35,71 @@ defmodule Bones73k.Accounts.User do
Otherwise databases may truncate the email without warnings, which Otherwise databases may truncate the email without warnings, which
could lead to unpredictable or insecure behaviour. Long passwords may could lead to unpredictable or insecure behaviour. Long passwords may
also be very expensive to hash for certain algorithms. also be very expensive to hash for certain algorithms.
## Options
* `:hash_password` - Hashes the password so it can be stored securely
in the database and ensures the password field is cleared to prevent
leaks in the logs. If password hashing is not needed and clearing the
password field is not desired (like when using this changeset for
validations on a LiveView form), this option can be set to `false`.
Defaults to `true`.
""" """
def registration_changeset(user, attrs) do def registration_changeset(user, attrs, opts \\ []) do
user user
|> cast(attrs, [:email, :password]) |> cast(attrs, [:email, :password, :role])
|> validate_role()
|> validate_email() |> validate_email()
|> validate_password() |> validate_password(opts)
end end
@doc """ defp role_validator(:role, role) do
A user changeset for registering admins. (RolesEnum.valid_value?(role) && []) || [role: "invalid user role"]
""" end
def admin_registration_changeset(user, attrs) do
user defp validate_role(changeset) do
|> registration_changeset(attrs) changeset
|> prepare_changes(&set_admin_role/1) |> validate_required([:role])
|> validate_change(:role, &role_validator/2)
end
defp validate_email_format(changeset) do
r_email = ~r/^[\w.!#$%&*+\-\/=?\^`{|}~]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i
changeset
|> validate_required([:email])
|> validate_format(:email, r_email, message: "must be a valid email address")
|> validate_length(:email, max: @max_email)
end end
defp validate_email(changeset) do defp validate_email(changeset) do
changeset changeset
|> validate_required([:email]) |> validate_email_format()
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> unsafe_validate_unique(:email, Bones73k.Repo) |> unsafe_validate_unique(:email, Bones73k.Repo)
|> unique_constraint(:email) |> unique_constraint(:email)
end end
defp validate_password(changeset) do defp validate_password(changeset, opts) do
changeset changeset
|> validate_required([:password]) |> validate_required([:password])
|> validate_length(:password, min: 12, max: 80) |> validate_length(:password, min: @min_password, max: @max_password)
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character") # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character") # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character") # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> prepare_changes(&hash_password/1) |> maybe_hash_password(opts)
end end
defp hash_password(changeset) do defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password) password = get_change(changeset, :password)
changeset if hash_password? && password && changeset.valid? do
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password)) changeset
|> delete_change(:password) |> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
end |> delete_change(:password)
else
defp set_admin_role(changeset) do changeset
changeset end
|> put_change(:role, :admin)
end end
@doc """ @doc """
@ -93,11 +120,11 @@ defmodule Bones73k.Accounts.User do
@doc """ @doc """
A user changeset for changing the password. A user changeset for changing the password.
""" """
def password_changeset(user, attrs) do def password_changeset(user, attrs, opts \\ []) do
user user
|> cast(attrs, [:password]) |> cast(attrs, [:password])
|> validate_confirmation(:password, message: "does not match password") |> validate_confirmation(:password, message: "does not match password")
|> validate_password() |> validate_password(opts)
end end
@doc """ @doc """

View file

@ -6,12 +6,15 @@ defmodule Bones73kWeb.UserRegistrationController do
alias Bones73kWeb.UserAuth alias Bones73kWeb.UserAuth
def new(conn, _params) do def new(conn, _params) do
changeset = Accounts.change_user_registration(%User{}) changeset = Accounts.change_user_registration(%User{}, %{role: Accounts.registration_role()})
render(conn, "new.html", changeset: changeset) render(conn, "new.html", changeset: changeset)
end end
def create(conn, %{"user" => user_params}) do def create(conn, %{"user" => user_params}) do
case Accounts.register_user(user_params) do user_params
|> Map.put_new("role", Accounts.registration_role())
|> Accounts.register_user()
|> case do
{:ok, user} -> {:ok, user} ->
%Bamboo.Email{} = %Bamboo.Email{} =
Accounts.deliver_user_confirmation_instructions( Accounts.deliver_user_confirmation_instructions(

View file

@ -1,22 +1,37 @@
<h1>Register</h1> <div class="row justify-content-center">
<div class="col-sm-9 col-md-7 col-lg-5 col-xl-4 ">
<%= form_for @changeset, Routes.user_registration_path(@conn, :create), fn f -> %> <h3>Register</h3>
<p class="lead">Registration gains additional features, like remembering your song request history.</p>
<%= form_for @changeset, Routes.user_registration_path(@conn, :create), form_opts(@changeset, novalidate: true), fn f -> %>
<%= if @changeset.action do %> <%= if @changeset.action do %>
<div class="alert alert-danger"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
<p>Oops, something went wrong! Please check the errors below.</p> Ope &mdash; please check the errors below.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
<% end %> <% end %>
<%= label f, :email %> <%= label f, :email, class: "form-label" %>
<%= email_input f, :email, required: true %> <div class="input-group has-validation mb-3">
<%= error_tag f, :email %> <span class="input-group-text" id="basic-addon1">
<%= icon_div @conn, "bi-at", [class: "icon fs-5"] %>
</span>
<%= email_input f, :email, class: error_class(f, :email, "form-control"), required: true %>
</div>
<%= error_tag f, :email, class: "d-block mt-n3 mb-3" %>
<%= label f, :password %> <%= label f, :password, class: "form-label" %>
<%= password_input f, :password, required: true %> <div class="input-group has-validation mb-3">
<%= error_tag f, :password %> <span class="input-group-text" id="basic-addon1">
<%= icon_div @conn, "bi-key", [class: "icon fs-5"] %>
</span>
<%= password_input f, :password, class: "form-control", required: true %>
</div>
<%= error_tag f, :password, class: "d-block mt-n3 mb-3" %>
<div> <div class="mb-3">
<%= submit "Register" %> <%= submit "Register", class: "btn btn-primary" %>
</div> </div>
<% end %> <% end %>
@ -25,6 +40,12 @@
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %> <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
</p> </p>
</div>
</div>
<h2>Lorem</h2> <h2>Lorem</h2>
<p> <p>

View file

@ -9,26 +9,30 @@
# #
# We recommend using the bang functions (`insert!`, `update!` # We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong. # and so on) as they will fail if something goes wrong.
alias Bones73k.Accounts
{:ok, admin} = {:ok, admin} =
Bones73k.Accounts.register_admin(%{ Accounts.register_user(%{
email: "admin@company.com", email: "admin@company.com",
password: "123456789abc", password: "123456789abc",
password_confirmation: "123456789abc" password_confirmation: "123456789abc",
role: Accounts.registration_role()
}) })
{:ok, user_1} = {:ok, user_1} =
Bones73k.Accounts.register_user(%{ Accounts.register_user(%{
email: "user1@company.com", email: "user1@company.com",
password: "123456789abc", password: "123456789abc",
password_confirmation: "123456789abc" password_confirmation: "123456789abc",
role: Accounts.registration_role()
}) })
{:ok, user_2} = {:ok, user_2} =
Bones73k.Accounts.register_user(%{ Accounts.register_user(%{
email: "user2@company.com", email: "user2@company.com",
password: "123456789abc", password: "123456789abc",
password_confirmation: "123456789abc" password_confirmation: "123456789abc",
role: Accounts.registration_role()
}) })
Enum.each(1..10, fn i -> Enum.each(1..10, fn i ->