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

View file

@ -5,9 +5,14 @@ defmodule Bones73k.Accounts.User do
defenum(RolesEnum, :role, [
:user,
:manager,
:admin
])
@max_email 254
@min_password 6
@max_password 80
@derive {Inspect, except: [:password]}
schema "users" do
field :email, :string
@ -19,6 +24,10 @@ defmodule Bones73k.Accounts.User do
timestamps()
end
def max_email, do: @max_email
def min_password, do: @min_password
def max_password, do: @max_password
@doc """
A user changeset for registration.
@ -26,53 +35,71 @@ defmodule Bones73k.Accounts.User do
Otherwise databases may truncate the email without warnings, which
could lead to unpredictable or insecure behaviour. Long passwords may
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
|> cast(attrs, [:email, :password])
|> cast(attrs, [:email, :password, :role])
|> validate_role()
|> validate_email()
|> validate_password()
|> validate_password(opts)
end
@doc """
A user changeset for registering admins.
"""
def admin_registration_changeset(user, attrs) do
user
|> registration_changeset(attrs)
|> prepare_changes(&set_admin_role/1)
defp role_validator(:role, role) do
(RolesEnum.valid_value?(role) && []) || [role: "invalid user role"]
end
defp validate_role(changeset) do
changeset
|> 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
defp validate_email(changeset) do
changeset
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> validate_email_format()
|> unsafe_validate_unique(:email, Bones73k.Repo)
|> unique_constraint(:email)
end
defp validate_password(changeset) do
defp validate_password(changeset, opts) do
changeset
|> 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 upper case 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
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)
changeset
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|> delete_change(:password)
end
defp set_admin_role(changeset) do
changeset
|> put_change(:role, :admin)
if hash_password? && password && changeset.valid? do
changeset
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
@doc """
@ -93,11 +120,11 @@ defmodule Bones73k.Accounts.User do
@doc """
A user changeset for changing the password.
"""
def password_changeset(user, attrs) do
def password_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:password])
|> validate_confirmation(:password, message: "does not match password")
|> validate_password()
|> validate_password(opts)
end
@doc """

View file

@ -6,12 +6,15 @@ defmodule Bones73kWeb.UserRegistrationController do
alias Bones73kWeb.UserAuth
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)
end
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} ->
%Bamboo.Email{} =
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 %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
Ope &mdash; please check the errors below.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<% end %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<%= error_tag f, :email %>
<%= label f, :email, class: "form-label" %>
<div class="input-group has-validation mb-3">
<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 %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :password %>
<%= label f, :password, class: "form-label" %>
<div class="input-group has-validation mb-3">
<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>
<%= submit "Register" %>
<div class="mb-3">
<%= submit "Register", class: "btn btn-primary" %>
</div>
<% end %>
@ -25,6 +40,12 @@
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
</p>
</div>
</div>
<h2>Lorem</h2>
<p>

View file

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