made changes to how roles are set on registering new users
This commit is contained in:
parent
2358b6adfb
commit
42b2ea9d78
5 changed files with 135 additions and 62 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
if hash_password? && password && changeset.valid? do
|
||||||
changeset
|
changeset
|
||||||
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|
||||||
|> delete_change(:password)
|
|> delete_change(:password)
|
||||||
end
|
else
|
||||||
|
|
||||||
defp set_admin_role(changeset) do
|
|
||||||
changeset
|
changeset
|
||||||
|> put_change(:role, :admin)
|
end
|
||||||
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 """
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 — 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>
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
Loading…
Reference in a new issue