user management with pagination & modal improvements
This commit is contained in:
parent
488a4e5195
commit
18468e3cc3
35 changed files with 2948 additions and 86 deletions
|
@ -28,3 +28,6 @@
|
||||||
.table-nonfluid {
|
.table-nonfluid {
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
}
|
}
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
|
@ -13,14 +13,28 @@ import "../node_modules/bootstrap-icons/icons/box-arrow-in-left.svg"; // log in
|
||||||
import "../node_modules/bootstrap-icons/icons/box-arrow-right.svg"; // log out
|
import "../node_modules/bootstrap-icons/icons/box-arrow-right.svg"; // log out
|
||||||
import "../node_modules/bootstrap-icons/icons/sliders.svg"; // new user / register
|
import "../node_modules/bootstrap-icons/icons/sliders.svg"; // new user / register
|
||||||
// forms etc
|
// forms etc
|
||||||
import "../node_modules/bootstrap-icons/icons/at.svg";
|
import "../node_modules/bootstrap-icons/icons/at.svg"; // email field
|
||||||
import "../node_modules/bootstrap-icons/icons/key.svg";
|
import "../node_modules/bootstrap-icons/icons/key.svg"; // new password field
|
||||||
import "../node_modules/bootstrap-icons/icons/key-fill.svg";
|
import "../node_modules/bootstrap-icons/icons/key-fill.svg"; // pw confirm field
|
||||||
import "../node_modules/bootstrap-icons/icons/lock.svg";
|
import "../node_modules/bootstrap-icons/icons/lock.svg"; // current pw field
|
||||||
import "../node_modules/bootstrap-icons/icons/shield-lock.svg";
|
import "../node_modules/bootstrap-icons/icons/shield-shaded.svg"; // role
|
||||||
import "../node_modules/bootstrap-icons/icons/arrow-repeat.svg";
|
// live tables
|
||||||
import "../node_modules/bootstrap-icons/icons/door-open.svg"; // log in
|
import "../node_modules/bootstrap-icons/icons/filter.svg";
|
||||||
import "../node_modules/@mdi/svg/svg/head-question-outline.svg"; // brand
|
import "../node_modules/bootstrap-icons/icons/backspace.svg"; // clear filter
|
||||||
|
import "../node_modules/bootstrap-icons/icons/sort-down-alt.svg";
|
||||||
|
import "../node_modules/bootstrap-icons/icons/sort-up-alt.svg";
|
||||||
|
import "../node_modules/bootstrap-icons/icons/chevron-left.svg";
|
||||||
|
import "../node_modules/bootstrap-icons/icons/chevron-right.svg";
|
||||||
|
import "../node_modules/bootstrap-icons/icons/pencil.svg";
|
||||||
|
import "../node_modules/bootstrap-icons/icons/trash.svg";
|
||||||
|
import "../node_modules/bootstrap-icons/icons/check.svg";
|
||||||
|
import "../node_modules/bootstrap-icons/icons/x.svg";
|
||||||
|
// page headers
|
||||||
|
import "../node_modules/bootstrap-icons/icons/shield-lock.svg"; // reset password
|
||||||
|
import "../node_modules/bootstrap-icons/icons/arrow-repeat.svg"; // resend confirmation
|
||||||
|
import "../node_modules/bootstrap-icons/icons/door-open.svg"; // log in header
|
||||||
|
import "../node_modules/@mdi/svg/svg/head-question-outline.svg"; // forgot password
|
||||||
|
import "../node_modules/bootstrap-icons/icons/people.svg"; // users management
|
||||||
|
|
||||||
// webpack automatically bundles all modules in your
|
// webpack automatically bundles all modules in your
|
||||||
// entry points. Those entry points can be configured
|
// entry points. Those entry points can be configured
|
||||||
|
|
52
assets/package-lock.json
generated
52
assets/package-lock.json
generated
|
@ -1305,9 +1305,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fontsource/lato": {
|
"node_modules/@fontsource/lato": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-4.2.2.tgz",
|
||||||
"integrity": "sha512-prsPmSttIWKF52AK6fNbe1a/wT3Rm5ho2qwegXlU0Qu72dksQIjGrttDtuvs3OlXdlmYXFTmLKOWQKwBMAsMmQ=="
|
"integrity": "sha512-ZE5WvqZQZinXpH8MaEiM9klDsUOfCHVQJ/tZKpNVQhi8mHt9WqPCROu500oI5jC3s6jaJuWsM7LfJ1zyEeW+XA=="
|
||||||
},
|
},
|
||||||
"node_modules/@mdi/svg": {
|
"node_modules/@mdi/svg": {
|
||||||
"version": "5.9.55",
|
"version": "5.9.55",
|
||||||
|
@ -2402,9 +2402,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/colorette": {
|
"node_modules/colorette": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||||
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
|
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
|
@ -5607,12 +5607,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.2.6",
|
"version": "8.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.7.tgz",
|
||||||
"integrity": "sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==",
|
"integrity": "sha512-DsVLH3xJzut+VT+rYr0mtvOtpTjSyqDwPf5EZWXcb0uAKfitGpTY9Ec+afi2+TgdN8rWS9Cs88UDYehKo/RvOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"colorette": "^1.2.1",
|
"colorette": "^1.2.2",
|
||||||
"nanoid": "^3.1.20",
|
"nanoid": "^3.1.20",
|
||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
},
|
},
|
||||||
|
@ -9484,9 +9484,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.24.2",
|
"version": "5.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.24.2.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.24.3.tgz",
|
||||||
"integrity": "sha512-uxxKYEY4kMNjP+D2Y+8aw5Vd7ar4pMuKCNemxV26ysr1nk0YDiQTylg9U3VZIdkmI0YHa0uC8ABxL+uGxGWWJg==",
|
"integrity": "sha512-x7lrWZ7wlWAdyKdML6YPvfVZkhD1ICuIZGODE5SzKJjqI9A4SpqGTjGJTc6CwaHqn19gGaoOR3ONJ46nYsn9rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.0",
|
"@types/eslint-scope": "^3.7.0",
|
||||||
|
@ -10859,9 +10859,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@fontsource/lato": {
|
"@fontsource/lato": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-4.2.2.tgz",
|
||||||
"integrity": "sha512-prsPmSttIWKF52AK6fNbe1a/wT3Rm5ho2qwegXlU0Qu72dksQIjGrttDtuvs3OlXdlmYXFTmLKOWQKwBMAsMmQ=="
|
"integrity": "sha512-ZE5WvqZQZinXpH8MaEiM9klDsUOfCHVQJ/tZKpNVQhi8mHt9WqPCROu500oI5jC3s6jaJuWsM7LfJ1zyEeW+XA=="
|
||||||
},
|
},
|
||||||
"@mdi/svg": {
|
"@mdi/svg": {
|
||||||
"version": "5.9.55",
|
"version": "5.9.55",
|
||||||
|
@ -11743,9 +11743,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorette": {
|
"colorette": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||||
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
|
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
|
@ -14160,12 +14160,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "8.2.6",
|
"version": "8.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.7.tgz",
|
||||||
"integrity": "sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==",
|
"integrity": "sha512-DsVLH3xJzut+VT+rYr0mtvOtpTjSyqDwPf5EZWXcb0uAKfitGpTY9Ec+afi2+TgdN8rWS9Cs88UDYehKo/RvOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"colorette": "^1.2.1",
|
"colorette": "^1.2.2",
|
||||||
"nanoid": "^3.1.20",
|
"nanoid": "^3.1.20",
|
||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
},
|
},
|
||||||
|
@ -17137,9 +17137,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"webpack": {
|
"webpack": {
|
||||||
"version": "5.24.2",
|
"version": "5.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.24.2.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.24.3.tgz",
|
||||||
"integrity": "sha512-uxxKYEY4kMNjP+D2Y+8aw5Vd7ar4pMuKCNemxV26ysr1nk0YDiQTylg9U3VZIdkmI0YHa0uC8ABxL+uGxGWWJg==",
|
"integrity": "sha512-x7lrWZ7wlWAdyKdML6YPvfVZkhD1ICuIZGODE5SzKJjqI9A4SpqGTjGJTc6CwaHqn19gGaoOR3ONJ46nYsn9rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/eslint-scope": "^3.7.0",
|
"@types/eslint-scope": "^3.7.0",
|
||||||
|
|
|
@ -10,6 +10,12 @@ use Mix.Config
|
||||||
config :bones73k,
|
config :bones73k,
|
||||||
ecto_repos: [Bones73k.Repo]
|
ecto_repos: [Bones73k.Repo]
|
||||||
|
|
||||||
|
# Custom application global variables
|
||||||
|
config :bones73k, :app_global_vars,
|
||||||
|
time_zone: "America/New_York",
|
||||||
|
mailer_reply_to: "reply_to@example.com",
|
||||||
|
mailer_from: "app_name@example.com"
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :bones73k, Bones73kWeb.Endpoint,
|
config :bones73k, Bones73kWeb.Endpoint,
|
||||||
url: [host: "localhost"],
|
url: [host: "localhost"],
|
||||||
|
|
|
@ -144,6 +144,40 @@ defmodule Bones73k.Accounts do
|
||||||
User.registration_changeset(user, attrs)
|
User.registration_changeset(user, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## User Management: updates, deletes
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns an `%Ecto.Changeset{}` for tracking user updates.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> change_user_update(user)
|
||||||
|
%Ecto.Changeset{data: %User{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def change_user_update(user, attrs \\ %{})
|
||||||
|
def change_user_update(nil, _), do: nil
|
||||||
|
|
||||||
|
def change_user_update(%User{} = user, attrs) do
|
||||||
|
User.update_changeset(user, attrs, hash_password: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @doc """
|
||||||
|
# Returns an `%Ecto.Changeset{}` for tracking singer_name updates.
|
||||||
|
# """
|
||||||
|
# def change_singer_name_update(%User{} = user, attrs \\ %{}) do
|
||||||
|
# User.update_singer_name_changeset(user, attrs)
|
||||||
|
# end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates the user given with attributes given
|
||||||
|
"""
|
||||||
|
def update_user(user, attrs) do
|
||||||
|
user
|
||||||
|
|> User.update_changeset(attrs, hash_password: true)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -398,4 +432,21 @@ defmodule Bones73k.Accounts do
|
||||||
{:error, :user, changeset, _} -> {:error, changeset}
|
{:error, :user, changeset, _} -> {:error, changeset}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## Management Actions
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Deletes a user.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> delete_user(user)
|
||||||
|
{:ok, %User{}}
|
||||||
|
|
||||||
|
iex> delete_user(user)
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def delete_user(nil), do: {:error, false}
|
||||||
|
def delete_user(%User{} = user), do: Repo.delete(user)
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,13 @@ defmodule Bones73k.Accounts.User do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import EctoEnum
|
import EctoEnum
|
||||||
|
|
||||||
defenum(RolesEnum, :role, [
|
@roles [
|
||||||
:user,
|
user: "Basic user level",
|
||||||
:manager,
|
manager: "Can create users, update user emails & passwords",
|
||||||
:admin
|
admin: "Can delete users and change user roles"
|
||||||
])
|
]
|
||||||
|
|
||||||
|
defenum(RolesEnum, :role, Keyword.keys(@roles))
|
||||||
|
|
||||||
@max_email 254
|
@max_email 254
|
||||||
@min_password 6
|
@min_password 6
|
||||||
|
@ -27,6 +29,7 @@ defmodule Bones73k.Accounts.User do
|
||||||
def max_email, do: @max_email
|
def max_email, do: @max_email
|
||||||
def min_password, do: @min_password
|
def min_password, do: @min_password
|
||||||
def max_password, do: @max_password
|
def max_password, do: @max_password
|
||||||
|
def roles, do: @roles
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
A user changeset for registration.
|
A user changeset for registration.
|
||||||
|
@ -53,6 +56,28 @@ defmodule Bones73k.Accounts.User do
|
||||||
|> validate_password(opts)
|
|> validate_password(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# def update_changeset(user, attrs, opts \\ [])
|
||||||
|
|
||||||
|
# def update_changeset(user, %{"password" => ""} = attrs, _),
|
||||||
|
# do: update_changeset_no_pw(user, attrs)
|
||||||
|
|
||||||
|
# def update_changeset(user, %{password: ""} = attrs, _), do: update_changeset_no_pw(user, attrs)
|
||||||
|
|
||||||
|
def update_changeset(user, attrs, opts) do
|
||||||
|
user
|
||||||
|
|> cast(attrs, [:email, :password, :role])
|
||||||
|
|> validate_role()
|
||||||
|
|> validate_email()
|
||||||
|
|> validate_password_not_required(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
# def update_changeset_no_pw(user, attrs) do
|
||||||
|
# user
|
||||||
|
# |> cast(attrs, [:email, :role])
|
||||||
|
# |> validate_role()
|
||||||
|
# |> validate_email()
|
||||||
|
# end
|
||||||
|
|
||||||
defp role_validator(:role, role) do
|
defp role_validator(:role, role) do
|
||||||
(RolesEnum.valid_value?(role) && []) || [role: "invalid user role"]
|
(RolesEnum.valid_value?(role) && []) || [role: "invalid user role"]
|
||||||
end
|
end
|
||||||
|
@ -82,6 +107,11 @@ defmodule Bones73k.Accounts.User do
|
||||||
defp validate_password(changeset, opts) do
|
defp validate_password(changeset, opts) do
|
||||||
changeset
|
changeset
|
||||||
|> validate_required([:password])
|
|> validate_required([:password])
|
||||||
|
|> validate_password_not_required(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_password_not_required(changeset, opts) do
|
||||||
|
changeset
|
||||||
|> validate_length(:password, min: @min_password, max: @max_password)
|
|> 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")
|
||||||
|
|
|
@ -2,4 +2,6 @@ defmodule Bones73k.Repo do
|
||||||
use Ecto.Repo,
|
use Ecto.Repo,
|
||||||
otp_app: :bones73k,
|
otp_app: :bones73k,
|
||||||
adapter: Ecto.Adapters.Postgres
|
adapter: Ecto.Adapters.Postgres
|
||||||
|
|
||||||
|
use Scrivener, page_size: 10
|
||||||
end
|
end
|
||||||
|
|
10
lib/bones73k/util/dt.ex
Normal file
10
lib/bones73k/util/dt.ex
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule Bones73k.Util.Dt do
|
||||||
|
@app_vars Application.get_env(:bones73k, :app_global_vars, time_zone: "America/New_York")
|
||||||
|
@time_zone @app_vars[:time_zone]
|
||||||
|
|
||||||
|
def ndt_to_local(%NaiveDateTime{} = ndt), do: Timex.to_datetime(ndt, @time_zone)
|
||||||
|
|
||||||
|
def format_dt_local(dt_local, fstr), do: Timex.format!(dt_local, fstr)
|
||||||
|
|
||||||
|
def format_ndt(%NaiveDateTime{} = ndt, fstr), do: ndt |> ndt_to_local() |> format_dt_local(fstr)
|
||||||
|
end
|
|
@ -140,6 +140,20 @@ defmodule Bones73kWeb.UserAuth do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Used for routes that require the user's email to be confirmed.
|
||||||
|
"""
|
||||||
|
def require_email_confirmed(conn, _opts) do
|
||||||
|
if conn.assigns[:current_user] |> Map.get(:confirmed_at) do
|
||||||
|
conn
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "You must confirm your email to access this page.")
|
||||||
|
|> redirect(to: Routes.user_confirmation_path(conn, :new))
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the pubsub topic name for receiving notifications when a user updated
|
Returns the pubsub topic name for receiving notifications when a user updated
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
defmodule Bones73kWeb.LiveHelpers do
|
defmodule Bones73kWeb.LiveHelpers do
|
||||||
import Phoenix.LiveView
|
import Phoenix.LiveView
|
||||||
|
import Phoenix.LiveView.Helpers
|
||||||
|
|
||||||
alias Bones73k.Accounts
|
alias Bones73k.Accounts
|
||||||
alias Bones73k.Accounts.User
|
alias Bones73k.Accounts.User
|
||||||
alias Bones73kWeb.UserAuth
|
alias Bones73kWeb.UserAuth
|
||||||
import Phoenix.LiveView.Helpers
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Performs the {:noreply, socket} for a given socket.
|
Performs the {:noreply, socket} for a given socket.
|
||||||
|
@ -32,8 +33,7 @@ defmodule Bones73kWeb.LiveHelpers do
|
||||||
return_to: Routes.property_index_path(@socket, :index) %>
|
return_to: Routes.property_index_path(@socket, :index) %>
|
||||||
"""
|
"""
|
||||||
def live_modal(socket, component, opts) do
|
def live_modal(socket, component, opts) do
|
||||||
path = Keyword.fetch!(opts, :return_to)
|
modal_opts = [id: :modal, component: component, opts: opts]
|
||||||
modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
|
|
||||||
# dirty little workaround for elixir complaining about socket being unused
|
# dirty little workaround for elixir complaining about socket being unused
|
||||||
_socket = socket
|
_socket = socket
|
||||||
live_component(socket, Bones73kWeb.ModalComponent, modal_opts)
|
live_component(socket, Bones73kWeb.ModalComponent, modal_opts)
|
||||||
|
@ -56,4 +56,14 @@ defmodule Bones73kWeb.LiveHelpers do
|
||||||
_ -> socket
|
_ -> socket
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Copies current flash into new put_flash invocations.
|
||||||
|
To be used before a push_patch.
|
||||||
|
"""
|
||||||
|
def copy_flash(%{assigns: %{flash: flash}} = socket) do
|
||||||
|
Enum.reduce(flash, socket, fn {k, v}, acc ->
|
||||||
|
put_flash(acc, String.to_existing_atom(k), v)
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,7 +31,8 @@ defmodule Bones73kWeb.ModalComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("close", _, socket) do
|
def handle_event("close", _, socket) do
|
||||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
send(self(), {:close_modal, true})
|
||||||
|
live_noreply(socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
@ -28,8 +28,10 @@ defmodule Bones73kWeb.PropertyLive.FormComponent do
|
||||||
defp save_property(socket, :edit, property_params) do
|
defp save_property(socket, :edit, property_params) do
|
||||||
case Properties.update_property(socket.assigns.property, property_params) do
|
case Properties.update_property(socket.assigns.property, property_params) do
|
||||||
{:ok, _property} ->
|
{:ok, _property} ->
|
||||||
|
flash = {:info, "Property updated successfully"}
|
||||||
|
send(self(), {:put_flash_message, flash})
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> put_flash(:info, "Property updated successfully")
|
|
||||||
|> push_event("modal-please-hide", %{})
|
|> push_event("modal-please-hide", %{})
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
|
|
||||||
|
@ -44,8 +46,10 @@ defmodule Bones73kWeb.PropertyLive.FormComponent do
|
||||||
|
|
||||||
case Properties.create_property(property_params) do
|
case Properties.create_property(property_params) do
|
||||||
{:ok, _property} ->
|
{:ok, _property} ->
|
||||||
|
flash = {:info, "Property created successfully"}
|
||||||
|
send(self(), {:put_flash_message, flash})
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> put_flash(:info, "Property created successfully")
|
|
||||||
|> push_event("modal-please-hide", %{})
|
|> push_event("modal-please-hide", %{})
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
|
|
||||||
|
|
|
@ -18,13 +18,16 @@ defmodule Bones73kWeb.PropertyLive.Index do
|
||||||
property = property_from_params(params)
|
property = property_from_params(params)
|
||||||
|
|
||||||
if Roles.can?(current_user, property, live_action) do
|
if Roles.can?(current_user, property, live_action) do
|
||||||
socket = assign(socket, :properties, list_properties())
|
socket
|
||||||
{:noreply, apply_action(socket, live_action, params)}
|
|> assign(:properties, list_properties())
|
||||||
|
|> assign(:modal_return_to, Routes.property_index_path(socket, :index))
|
||||||
|
|> apply_action(live_action, params)
|
||||||
|
|> live_noreply()
|
||||||
else
|
else
|
||||||
{:noreply,
|
socket
|
||||||
socket
|
|> put_flash(:error, "Unauthorised")
|
||||||
|> put_flash(:error, "Unauthorised")
|
|> redirect(to: "/")
|
||||||
|> redirect(to: "/")}
|
|> live_noreply()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -64,6 +67,16 @@ defmodule Bones73kWeb.PropertyLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do
|
||||||
|
socket |> copy_flash() |> push_patch(to: to) |> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
|
||||||
|
socket |> put_flash(flash_type, msg) |> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
defp property_from_params(params)
|
defp property_from_params(params)
|
||||||
|
|
||||||
defp property_from_params(%{"id" => id}),
|
defp property_from_params(%{"id" => id}),
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
<div class="d-flex justify-content-between d-flex align-items-end">
|
|
||||||
<h2>Listing Properties</h2>
|
|
||||||
<span><%= live_patch "New Property", to: Routes.property_index_path(@socket, :new), class: "btn btn-primary" %></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= if @live_action in [:new, :edit] do %>
|
<%= if @live_action in [:new, :edit] do %>
|
||||||
<%= live_modal @socket, Bones73kWeb.PropertyLive.FormComponent,
|
<%= live_modal @socket, Bones73kWeb.PropertyLive.FormComponent,
|
||||||
id: @property.id || :new,
|
id: @property.id || :new,
|
||||||
title: @page_title,
|
title: @page_title,
|
||||||
action: @live_action,
|
action: @live_action,
|
||||||
property: @property,
|
property: @property,
|
||||||
current_user: @current_user,
|
current_user: @current_user %>
|
||||||
return_to: Routes.property_index_path(@socket, :index) %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between d-flex align-items-end">
|
||||||
|
<h2>Listing Properties</h2>
|
||||||
|
<span><%= live_patch "New Property", to: Routes.property_index_path(@socket, :new), class: "btn btn-primary" %></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -40,7 +40,7 @@ defmodule Bones73kWeb.UserLive.Registration do
|
||||||
|> Accounts.register_user()
|
|> Accounts.register_user()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
%Bamboo.Email{} =
|
{:ok, %Bamboo.Email{}} =
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
Accounts.deliver_user_confirmation_instructions(
|
||||||
user,
|
user,
|
||||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-at", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-at", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
value: input_value(f, :email),
|
value: input_value(f, :email),
|
||||||
|
@ -32,14 +32,13 @@
|
||||||
<%= label f, :password, class: "form-label" %>
|
<%= label f, :password, class: "form-label" %>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-key", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
class: input_class(f, :password, "form-control"),
|
class: input_class(f, :password, "form-control"),
|
||||||
maxlength: User.max_password,
|
maxlength: User.max_password,
|
||||||
required: true,
|
phx_debounce: "250",
|
||||||
phx_debounce: "200",
|
|
||||||
aria_describedby: error_id(f, :password)
|
aria_describedby: error_id(f, :password)
|
||||||
%>
|
%>
|
||||||
<%= error_tag f, :password %>
|
<%= error_tag f, :password %>
|
||||||
|
@ -47,7 +46,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<%= submit (@trigger_submit && "Saving..." || "Register"), disabled: @trigger_submit || !@changeset.valid?, class: "btn btn-primary", phx_disable_with: "Saving..." %>
|
<%= submit (@trigger_submit && "Saving..." || "Register"),
|
||||||
|
class: "btn btn-primary",
|
||||||
|
disabled: @trigger_submit || !@changeset.valid?,
|
||||||
|
aria_disabled: (@trigger_submit || !@changeset.valid?) && "true" || false,
|
||||||
|
phx_disable_with: "Saving..."
|
||||||
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<%= label f, :password, "New password", class: "form-label" %>
|
<%= label f, :password, "New password", class: "form-label" %>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-key", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-key-fill", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-key-fill", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= password_input f, :password_confirmation,
|
<%= password_input f, :password_confirmation,
|
||||||
value: input_value(f, :password_confirmation),
|
value: input_value(f, :password_confirmation),
|
||||||
|
@ -43,7 +43,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<%= submit "Reset password", disabled: !@changeset.valid?, class: "btn btn-primary", phx_disable_with: "Saving..." %>
|
<%= submit "Reset password",
|
||||||
|
class: "btn btn-primary",
|
||||||
|
disabled: !@changeset.valid?,
|
||||||
|
aria_disabled: !@changeset.valid? && "true" || false,
|
||||||
|
phx_disable_with: "Saving..."
|
||||||
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-at", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-at", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
value: input_value(f, :email),
|
value: input_value(f, :email),
|
||||||
class: input_class(f, :email, "form-control"),
|
class: input_class(f, :email, "form-control"),
|
||||||
placeholder: "e.g., babka@73k.us",
|
placeholder: "e.g., babka@73k.us",
|
||||||
maxlength: User.max_email,
|
maxlength: User.max_email,
|
||||||
phx_debounce: "600",
|
phx_debounce: "500",
|
||||||
aria_describedby: error_id(f, :email)
|
aria_describedby: error_id(f, :email)
|
||||||
%>
|
%>
|
||||||
<%= error_tag f, :email %>
|
<%= error_tag f, :email %>
|
||||||
|
@ -27,12 +27,11 @@
|
||||||
<%= label f, :current_password, class: "form-label" %>
|
<%= label f, :current_password, class: "form-label" %>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-lock", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-lock", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= password_input f, :current_password,
|
<%= password_input f, :current_password,
|
||||||
value: input_value(f, :current_password),
|
value: input_value(f, :current_password),
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
required: true,
|
|
||||||
aria_describedby: error_id(f, :current_password)
|
aria_describedby: error_id(f, :current_password)
|
||||||
%>
|
%>
|
||||||
<%= error_tag f, :current_password %>
|
<%= error_tag f, :current_password %>
|
||||||
|
@ -41,8 +40,9 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<%= submit "Change email",
|
<%= submit "Change email",
|
||||||
disabled: !@changeset.valid? || input_value(f, :current_password) == "",
|
|
||||||
class: "btn btn-primary",
|
class: "btn btn-primary",
|
||||||
|
disabled: !@changeset.valid? || input_value(f, :current_password) == "",
|
||||||
|
aria_disabled: (!@changeset.valid? || input_value(f, :current_password) == "") && "true" || false,
|
||||||
phx_disable_with: "Saving..."
|
phx_disable_with: "Saving..."
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,13 +8,13 @@
|
||||||
<%= label f, :password, "New password", class: "form-label" %>
|
<%= label f, :password, "New password", class: "form-label" %>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-key", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-key", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
value: input_value(f, :password),
|
value: input_value(f, :password),
|
||||||
class: input_class(f, :password, "form-control"),
|
class: input_class(f, :password, "form-control"),
|
||||||
maxlength: User.max_password,
|
maxlength: User.max_password,
|
||||||
phx_debounce: "600",
|
phx_debounce: "500",
|
||||||
aria_describedby: error_id(f, :password)
|
aria_describedby: error_id(f, :password)
|
||||||
%>
|
%>
|
||||||
<%= error_tag f, :password %>
|
<%= error_tag f, :password %>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-key-fill", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-key-fill", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= password_input f, :password_confirmation,
|
<%= password_input f, :password_confirmation,
|
||||||
value: input_value(f, :password_confirmation),
|
value: input_value(f, :password_confirmation),
|
||||||
|
@ -41,12 +41,11 @@
|
||||||
<%= label f, :current_password, class: "form-label" %>
|
<%= label f, :current_password, class: "form-label" %>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @socket, "bi-lock", [class: "icon fs-5"] %>
|
<%= icon_div @socket, "bi-lock", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= password_input f, :current_password,
|
<%= password_input f, :current_password,
|
||||||
value: input_value(f, :current_password),
|
value: input_value(f, :current_password),
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
required: true,
|
|
||||||
aria_describedby: error_id(f, :current_password)
|
aria_describedby: error_id(f, :current_password)
|
||||||
%>
|
%>
|
||||||
<%= error_tag f, :current_password %>
|
<%= error_tag f, :current_password %>
|
||||||
|
@ -55,8 +54,9 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<%= submit "Change password",
|
<%= submit "Change password",
|
||||||
disabled: !@changeset.valid? || input_value(f, :current_password) == "",
|
|
||||||
class: "btn btn-primary",
|
class: "btn btn-primary",
|
||||||
|
disabled: !@changeset.valid? || input_value(f, :current_password) == "",
|
||||||
|
aria_disabled: (!@changeset.valid? || input_value(f, :current_password) == "") && "true" || false,
|
||||||
phx_disable_with: "Saving..."
|
phx_disable_with: "Saving..."
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
|
|
91
lib/bones73k_web/live/user_management/form_component.ex
Normal file
91
lib/bones73k_web/live/user_management/form_component.ex
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
defmodule Bones73kWeb.UserManagement.FormComponent do
|
||||||
|
use Bones73kWeb, :live_component
|
||||||
|
|
||||||
|
alias Bones73k.{Repo, Accounts}
|
||||||
|
alias Bones73k.Accounts.User
|
||||||
|
alias Bones73kWeb.Roles
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket
|
||||||
|
|> assign(assigns)
|
||||||
|
|> init_changeset(assigns)
|
||||||
|
|> assign(:role_id, 1)
|
||||||
|
|> live_okreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp init_changeset(socket, %{action: :new, user: user}) do
|
||||||
|
params = %{role: Accounts.registration_role()}
|
||||||
|
assign(socket, changeset: Accounts.change_user_registration(user, params))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp init_changeset(socket, %{action: :edit, user: user}) do
|
||||||
|
assign(socket, changeset: Accounts.change_user_update(user))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_changes(%{action: :new, user: user}, user_params) do
|
||||||
|
Accounts.change_user_registration(user, user_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_changes(%{action: :edit, user: user}, user_params) do
|
||||||
|
Accounts.change_user_update(user, user_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_user(%{assigns: %{action: :new}} = socket, user_params) do
|
||||||
|
case Accounts.register_user(user_params) do
|
||||||
|
{:ok, user} ->
|
||||||
|
{:ok, %Bamboo.Email{}} =
|
||||||
|
Accounts.deliver_user_confirmation_instructions(
|
||||||
|
user,
|
||||||
|
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||||
|
)
|
||||||
|
|
||||||
|
flash = {:success, "User created successfully: #{user.email}"}
|
||||||
|
send(self(), {:put_flash_message, flash})
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> push_event("modal-please-hide", %{})
|
||||||
|
|> live_noreply()
|
||||||
|
|
||||||
|
{:error, cs} ->
|
||||||
|
socket
|
||||||
|
|> put_flash(:error, "Some error creating this user...")
|
||||||
|
|> assign(changeset: cs)
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_user(%{assigns: %{action: :edit, user: user}} = socket, user_params) do
|
||||||
|
case Accounts.update_user(user, user_params) do
|
||||||
|
{:ok, user} ->
|
||||||
|
flash = {:info, "User updated successfully: #{user.email}"}
|
||||||
|
send(self(), {:put_flash_message, flash})
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> push_event("modal-please-hide", %{})
|
||||||
|
|> live_noreply()
|
||||||
|
|
||||||
|
{:error, cs} ->
|
||||||
|
{:noreply, assign(socket, :changeset, cs)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||||
|
cs = validate_changes(socket.assigns, user_params)
|
||||||
|
{:noreply, assign(socket, :changeset, %{cs | action: :validate})}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("save", %{"user" => user_params}, socket) do
|
||||||
|
save_user(socket, user_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def role_description(role) when is_atom(role) do
|
||||||
|
Keyword.get(User.roles(), role)
|
||||||
|
end
|
||||||
|
|
||||||
|
def role_description(role) when is_binary(role) do
|
||||||
|
Keyword.get(User.roles(), String.to_existing_atom(role))
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,67 @@
|
||||||
|
<%= form_for @changeset, "#", [
|
||||||
|
phx_target: @myself,
|
||||||
|
phx_change: "validate",
|
||||||
|
phx_submit: "save"
|
||||||
|
], fn f -> %>
|
||||||
|
|
||||||
|
<div class="mb-3" phx-feedback-for="<%= input_id(f, :email)%>">
|
||||||
|
<%= label f, :email, class: "form-label" %>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<%= icon_div @socket, "bi-at", [class: "icon"] %>
|
||||||
|
</span>
|
||||||
|
<%= email_input f, :email,
|
||||||
|
value: input_value(f, :email),
|
||||||
|
class: input_class(f, :email, "form-control"),
|
||||||
|
placeholder: "e.g., babka@73k.us",
|
||||||
|
maxlength: User.max_email,
|
||||||
|
autofocus: true,
|
||||||
|
phx_debounce: "250",
|
||||||
|
aria_describedby: error_id(f, :email)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :email %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<%= if Roles.can?(@current_user, %User{}, :edit_role) do %>
|
||||||
|
<%= label f, :role, class: "form-label" %>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<%= icon_div @socket, "bi-shield-shaded", [class: "icon"] %>
|
||||||
|
</span>
|
||||||
|
<%= select f, :role, Enum.map(User.roles(), fn {k, v} -> {String.capitalize(Atom.to_string(k)), k} end), class: "form-select" %>
|
||||||
|
<span class="valid-feedback text-primary" style="display: block;">
|
||||||
|
<%= role_description(input_value(f, :role)) %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<%= hidden_input f, :role, value: input_value(f, :role) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
||||||
|
<%= label f, :password, class: "form-label" %>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<%= icon_div @socket, "bi-key", [class: "icon"] %>
|
||||||
|
</span>
|
||||||
|
<%= password_input f, :password,
|
||||||
|
value: input_value(f, :password),
|
||||||
|
class: input_class(f, :password, "form-control"),
|
||||||
|
maxlength: User.max_password,
|
||||||
|
aria_describedby: error_id(f, :password)
|
||||||
|
%>
|
||||||
|
<%= error_tag f, :password %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<%= submit "Save",
|
||||||
|
class: "btn btn-primary",
|
||||||
|
disabled: !@changeset.valid?,
|
||||||
|
aria_disabled: !@changeset.valid? && "true" || false,
|
||||||
|
phx_disable_with: "Saving..."
|
||||||
|
%>
|
||||||
|
|
||||||
|
<% end %>
|
204
lib/bones73k_web/live/user_management/index.ex
Normal file
204
lib/bones73k_web/live/user_management/index.ex
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
defmodule Bones73kWeb.UserManagementLive.Index do
|
||||||
|
use Bones73kWeb, :live_view
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
import Bones73kWeb.Pagination
|
||||||
|
import Bones73k.Util.Dt
|
||||||
|
|
||||||
|
alias Bones73k.Repo
|
||||||
|
alias Bones73k.Accounts
|
||||||
|
alias Bones73k.Accounts.User
|
||||||
|
alias Bones73kWeb.Roles
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, session, socket) do
|
||||||
|
socket
|
||||||
|
|> assign_defaults(session)
|
||||||
|
|> live_okreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_params(params, _url, socket) do
|
||||||
|
current_user = socket.assigns.current_user
|
||||||
|
live_action = socket.assigns.live_action
|
||||||
|
user = user_from_params(params)
|
||||||
|
|
||||||
|
if Roles.can?(current_user, user, live_action) do
|
||||||
|
socket
|
||||||
|
|> assign(:query, query_map(params))
|
||||||
|
|> assign_modal_return_to()
|
||||||
|
|> page_query()
|
||||||
|
|> apply_action(socket.assigns.live_action, params)
|
||||||
|
|> live_noreply()
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
|> put_flash(:error, "Unauthorised")
|
||||||
|
|> redirect(to: "/")
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :index, _params) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Listing Users")
|
||||||
|
|> assign(:user, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :new, _params) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "New User")
|
||||||
|
|> assign(:user, %User{})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Edit User")
|
||||||
|
|> assign(:user, Accounts.get_user!(id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_modal_return_to(%{assigns: %{query: query}} = socket) do
|
||||||
|
to = Routes.user_management_index_path(socket, :index, Enum.into(query, []))
|
||||||
|
assign(socket, :modal_return_to, to)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp user_from_params(params)
|
||||||
|
|
||||||
|
defp user_from_params(%{"id" => id}),
|
||||||
|
do: Accounts.get_user!(id)
|
||||||
|
|
||||||
|
defp user_from_params(_params), do: %User{}
|
||||||
|
|
||||||
|
def query_map(params) do
|
||||||
|
%{
|
||||||
|
filter: params["filter"] || "",
|
||||||
|
sort_by: (params["sort_by"] in ~w(email inserted_at role) && params["sort_by"]) || "email",
|
||||||
|
sort_order: (params["sort_order"] == "desc" && "desc") || "asc",
|
||||||
|
page_number: String.to_integer(params["page_number"] || "1"),
|
||||||
|
page_size: String.to_integer(params["page_size"] || "10")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp page_query(%{assigns: %{query: query}} = socket) do
|
||||||
|
result_page =
|
||||||
|
from(u in User)
|
||||||
|
|> or_where([u], ilike(u.email, ^"%#{query.filter}%"))
|
||||||
|
# |> or_where([u], ilike(u.singer_name, ^"%#{query.filter}%"))
|
||||||
|
|> order_by([u], [
|
||||||
|
{^String.to_existing_atom(query.sort_order), ^String.to_existing_atom(query.sort_by)}
|
||||||
|
])
|
||||||
|
|> Repo.paginate(page: query.page_number, page_size: query.page_size)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:page, result_page)
|
||||||
|
|> assign(:table_loading, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("delete", %{"id" => id, "email" => email}, socket) do
|
||||||
|
id
|
||||||
|
|> Accounts.get_user()
|
||||||
|
|> Accounts.delete_user()
|
||||||
|
|> case do
|
||||||
|
{:ok, _} ->
|
||||||
|
socket
|
||||||
|
|> put_flash(:success, "User deleted successfully: \"#{email}\"")
|
||||||
|
|> assign(:table_loading, true)
|
||||||
|
|> page_query()
|
||||||
|
|> live_noreply()
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
socket
|
||||||
|
|> put_flash(
|
||||||
|
:error,
|
||||||
|
"Something went wrong attempting to delete user \"#{email}\". Possibly already deleted? Reloading list..."
|
||||||
|
)
|
||||||
|
|> assign(:table_loading, true)
|
||||||
|
|> page_query()
|
||||||
|
|> live_noreply()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("filter-change", params, socket) do
|
||||||
|
send(self(), {:query_update, Map.put(params, "page_number", "1")})
|
||||||
|
{:noreply, assign(socket, :table_loading, true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("filter-clear", _params, socket) do
|
||||||
|
send(self(), {:query_update, %{"filter" => "", "page_number" => "1"}})
|
||||||
|
{:noreply, assign(socket, :table_loading, true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event(
|
||||||
|
"sort-change",
|
||||||
|
%{"sort_by" => column} = params,
|
||||||
|
%{assigns: %{query: query}} = socket
|
||||||
|
) do
|
||||||
|
(column == query.sort_by &&
|
||||||
|
send(
|
||||||
|
self(),
|
||||||
|
{:query_update, %{"sort_order" => (query.sort_order == "asc" && "desc") || "asc"}}
|
||||||
|
)) ||
|
||||||
|
send(self(), {:query_update, Map.put(params, "sort_order", "asc")})
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :table_loading, true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("sort-by-change", %{"sort" => params}, socket) do
|
||||||
|
send(self(), {:query_update, params})
|
||||||
|
{:noreply, assign(socket, :table_loading, true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("sort-order-change", _params, socket) do
|
||||||
|
new_sort_order = (socket.assigns.query.sort_order == "asc" && "desc") || "asc"
|
||||||
|
send(self(), {:query_update, %{"sort_order" => new_sort_order}})
|
||||||
|
{:noreply, assign(socket, :table_loading, true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("page-change", params, socket) do
|
||||||
|
send(self(), {:query_update, params})
|
||||||
|
{:noreply, assign(socket, :table_loading, true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("page-size-change", params, socket) do
|
||||||
|
send(self(), {:query_update, Map.put(params, "page_number", "1")})
|
||||||
|
{:noreply, assign(socket, :table_loading, true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:query_update, params}, %{assigns: %{query: q}} = socket) do
|
||||||
|
{:noreply,
|
||||||
|
push_patch(socket,
|
||||||
|
to: Routes.user_management_index_path(socket, :index, get_new_params(params, q)),
|
||||||
|
replace: true
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do
|
||||||
|
socket |> copy_flash() |> push_patch(to: to) |> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
|
||||||
|
socket |> put_flash(flash_type, msg) |> live_noreply()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_new_params(params, query) do
|
||||||
|
[
|
||||||
|
{:filter, Map.get(params, "filter") || query.filter},
|
||||||
|
{:sort_by, Map.get(params, "sort_by") || query.sort_by},
|
||||||
|
{:sort_order, Map.get(params, "sort_order") || query.sort_order},
|
||||||
|
{:page_number, Map.get(params, "page_number") || query.page_number},
|
||||||
|
{:page_size, Map.get(params, "page_size") || query.page_size}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def dt_out(ndt), do: format_ndt(ndt, "{YYYY} {Mshort} {0D}, {h12}:{0m} {am}")
|
||||||
|
end
|
195
lib/bones73k_web/live/user_management/index.html.leex
Normal file
195
lib/bones73k_web/live/user_management/index.html.leex
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
<%= if @live_action in [:new, :edit] do %>
|
||||||
|
<%= live_modal @socket, Bones73kWeb.UserManagement.FormComponent,
|
||||||
|
id: @user.id || :new,
|
||||||
|
title: @page_title,
|
||||||
|
action: @live_action,
|
||||||
|
user: @user,
|
||||||
|
current_user: @current_user %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
<h2 class="mb-3">
|
||||||
|
<%= icon_div @socket, "bi-people", [class: "icon baseline"] %>
|
||||||
|
Listing Users
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<%# filtering and new item creation %>
|
||||||
|
<div class="d-flex flex-column flex-sm-row justify-content-between d-flex align-items-start">
|
||||||
|
|
||||||
|
<%= live_patch to: Routes.user_management_index_path(@socket, :new, Enum.into(@query, [])),
|
||||||
|
class: "btn btn-primary mb-2" do %>
|
||||||
|
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline me-1"] %>
|
||||||
|
New User
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %>
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
|
||||||
|
<span class="input-group-text">
|
||||||
|
<%= icon_div @socket, "bi-filter", [class: "icon"] %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<%= text_input flt, :filter, name: "filter", class: "form-control", placeholder: "Filter users...", value: @query.filter %>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<%= if @query.filter == "" do %>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" aria-label="Clear filter" aria-disabled="true" disabled>
|
||||||
|
<% else %>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" aria-label="Clear filter" phx-click="filter-clear">
|
||||||
|
<% end %>
|
||||||
|
<%= icon_div @socket, "bi-backspace", [class: "icon baseline"], [role: "img", aria_hidden: false] %>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# main data table %>
|
||||||
|
<table class="table">
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr class="<%= @table_loading && "loading" || "" %>">
|
||||||
|
|
||||||
|
<th scope="col" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
|
||||||
|
Email
|
||||||
|
<%= if @query.sort_by == "email", do: icon_div @socket,
|
||||||
|
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||||
|
[class: "icon baseline ms-1"]
|
||||||
|
%>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" phx-click="sort-change" phx-value-sort_by="role" class="cursor-pointer">
|
||||||
|
Role
|
||||||
|
<%= if @query.sort_by == "role", do: icon_div @socket,
|
||||||
|
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||||
|
[class: "icon baseline ms-1"]
|
||||||
|
%>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" phx-click="sort-change" phx-value-sort_by="inserted_at" class="cursor-pointer">
|
||||||
|
Created at
|
||||||
|
<%= if @query.sort_by == "inserted_at", do: icon_div @socket,
|
||||||
|
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||||
|
[class: "icon baseline ms-1"]
|
||||||
|
%>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col">Confirmed?</th>
|
||||||
|
|
||||||
|
<th></th>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody id="users">
|
||||||
|
<%= for user <- @page.entries do %>
|
||||||
|
<tr id="user-<%= user.id %>">
|
||||||
|
<td class="align-middle"><%= user.email %></td>
|
||||||
|
<td class="align-middle"><%= user.role |> Atom.to_string() |> String.capitalize() %></td>
|
||||||
|
<td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<%= if user.confirmed_at do %>
|
||||||
|
<span class="visually-hidden">Yes</span>
|
||||||
|
<%= icon_div @socket, "bi-check", [class: "icon baseline fs-4 text-success"], [role: "img", aria_hidden: false] %>
|
||||||
|
<% else %>
|
||||||
|
<span class="visually-hidden">No</span>
|
||||||
|
<%= icon_div @socket, "bi-x", [class: "icon baseline fs-4 text-warning"], [role: "img", aria_hidden: false] %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle text-end">
|
||||||
|
|
||||||
|
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm" do %>
|
||||||
|
<%= icon_div @socket, "bi-pencil", [class: "icon baseline", style: "margin-right:0.125rem;"] %>
|
||||||
|
Edit
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if Roles.can?(@current_user, user, :delete) do %>
|
||||||
|
<%= link to: "#",
|
||||||
|
phx_click: "delete",
|
||||||
|
phx_value_id: user.id,
|
||||||
|
phx_value_email: user.email,
|
||||||
|
data: [confirm: "Are you sure you want to delete this user? \"#{user.email}\""],
|
||||||
|
class: "btn btn-outline-danger btn-sm" do %>
|
||||||
|
<%= icon_div @socket, "bi-trash", [class: "icon baseline", style: "margin-right:0.125rem;"] %>
|
||||||
|
Delete
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<%# pagination interface %>
|
||||||
|
<div class="d-flex justify-content-between d-flex align-items-start">
|
||||||
|
|
||||||
|
<%# items per page selector %>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<%= form_for :page_size, "#", [phx_change: "page-size-change"], fn pgsz -> %>
|
||||||
|
<%= select pgsz, :page_size,
|
||||||
|
[10, 15, 20, 30, 50, 100] |> Enum.map(fn n -> {"#{n} per page", n} end),
|
||||||
|
value: @query.page_size,
|
||||||
|
id: "table_page_size_page_size",
|
||||||
|
name: "page_size",
|
||||||
|
class: "form-select"
|
||||||
|
%>
|
||||||
|
<% end %>
|
||||||
|
<span class="ms-2"><%= @page.total_entries %> found</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# main pagination %>
|
||||||
|
<nav aria-label="User list page navigation">
|
||||||
|
<ul class="pagination mb-0">
|
||||||
|
|
||||||
|
<%# previous page button %>
|
||||||
|
<% icon = icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
||||||
|
<%= if @page.page_number == 1 do %>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
||||||
|
<span class="visually-hidden">Previous</span>
|
||||||
|
<% else %>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="#" aria-label="Previous" phx-value-page_number="<%= @page.page_number - 1 %>" phx-click="page-change"><%= icon %></a>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<%# page buttons %>
|
||||||
|
<%= for page_num <- generate_page_list(@page.page_number, @page.total_pages) do %>
|
||||||
|
<%= cond do %>
|
||||||
|
<%= page_num < 1 -> %>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link" aria-hidden="true">…</span>
|
||||||
|
<span class="visually-hidden" role="img" aria-label="ellipses">…</span>
|
||||||
|
</li>
|
||||||
|
<%= page_num == @page.page_number -> %>
|
||||||
|
<li class="page-item active" aria-current="page">
|
||||||
|
<span class="page-link"><%= page_num %></a>
|
||||||
|
</li>
|
||||||
|
<%= true -> %>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="#" phx-value-page_number="<%= page_num %>" phx-click="page-change"><%= page_num %></a>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%# next page button %>
|
||||||
|
<% icon = icon_div @socket, "bi-chevron-right", [class: "icon baseline"] %>
|
||||||
|
<%= if @page.page_number == @page.total_pages do %>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
||||||
|
<span class="visually-hidden">Next</span>
|
||||||
|
<% else %>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="#" aria-label="Next" phx-value-page_number="<%= @page.page_number + 1 %>" phx-click="page-change"><%= icon %></a>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
|
@ -7,15 +7,26 @@ defmodule Bones73kWeb.Roles do
|
||||||
alias Bones73k.Properties.Property
|
alias Bones73k.Properties.Property
|
||||||
|
|
||||||
@type entity :: struct()
|
@type entity :: struct()
|
||||||
@type action :: :new | :index | :edit | :show | :delete
|
@type action :: :new | :index | :edit | :show | :delete | :edit_role
|
||||||
@spec can?(%User{}, entity(), action()) :: boolean()
|
@spec can?(%User{}, entity(), action()) :: boolean()
|
||||||
|
|
||||||
def can?(user, entity, action)
|
def can?(user, entity, action)
|
||||||
|
|
||||||
|
# Properties / Property
|
||||||
def can?(%User{role: :admin}, %Property{}, _any), do: true
|
def can?(%User{role: :admin}, %Property{}, _any), do: true
|
||||||
def can?(%User{}, %Property{}, :index), do: true
|
def can?(%User{}, %Property{}, :index), do: true
|
||||||
def can?(%User{}, %Property{}, :new), do: true
|
def can?(%User{}, %Property{}, :new), do: true
|
||||||
def can?(%User{}, %Property{}, :show), do: true
|
def can?(%User{}, %Property{}, :show), do: true
|
||||||
def can?(%User{id: id}, %Property{user_id: id}, :edit), do: true
|
def can?(%User{id: id}, %Property{user_id: id}, :edit), do: true
|
||||||
def can?(%User{id: id}, %Property{user_id: id}, :delete), do: true
|
def can?(%User{id: id}, %Property{user_id: id}, :delete), do: true
|
||||||
|
|
||||||
|
# Accounts / User
|
||||||
|
def can?(%User{role: :admin}, %User{}, _any), do: true
|
||||||
|
def can?(%User{role: :manager}, %User{}, :index), do: true
|
||||||
|
def can?(%User{role: :manager}, %User{}, :new), do: true
|
||||||
|
def can?(%User{role: :manager}, %User{}, :edit), do: true
|
||||||
|
def can?(%User{role: :manager}, %User{}, :show), do: true
|
||||||
|
|
||||||
|
# Final response
|
||||||
def can?(_, _, _), do: false
|
def can?(_, _, _), do: false
|
||||||
end
|
end
|
||||||
|
|
|
@ -105,4 +105,14 @@ defmodule Bones73kWeb.Router do
|
||||||
|
|
||||||
live("/admin_dashboard", AdminDashboardLive, :index)
|
live("/admin_dashboard", AdminDashboardLive, :index)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Users Management
|
||||||
|
scope "/users", Bones73kWeb do
|
||||||
|
pipe_through [:browser, :require_authenticated_user, :manager, :require_email_confirmed]
|
||||||
|
|
||||||
|
live("/", UserManagementLive.Index, :index)
|
||||||
|
live("/new", UserManagementLive.Index, :new)
|
||||||
|
live("/edit/:id", UserManagementLive.Index, :edit)
|
||||||
|
# resources "/", UserManagementController, only: [:new, :create, :edit, :update]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,15 @@
|
||||||
|
|
||||||
<li><%= content_tag :span, @current_user.email, class: "dropdown-item-text" %></li>
|
<li><%= content_tag :span, @current_user.email, class: "dropdown-item-text" %></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<%= if Roles.can?(@current_user, %User{}, :index) do %>
|
||||||
|
<li>
|
||||||
|
<%= link nav_link_opts(@conn, to: Routes.user_management_index_path(@conn, :index), class: "dropdown-item") do %>
|
||||||
|
<%= icon_div @conn, "bi-people", [class: "icon baseline me-1"] %>
|
||||||
|
Users
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<% end %>
|
||||||
<li>
|
<li>
|
||||||
<%= link nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") do %>
|
<%= link nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") do %>
|
||||||
<%= icon_div @conn, "bi-sliders", [class: "icon baseline me-1"] %>
|
<%= icon_div @conn, "bi-sliders", [class: "icon baseline me-1"] %>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="input-group has-validation mb-3">
|
<div class="input-group has-validation mb-3">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @conn, "bi-at", [class: "icon fs-5"] %>
|
<%= icon_div @conn, "bi-at", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
placeholder: "e.g., babka@73k.us",
|
placeholder: "e.g., babka@73k.us",
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<%= label f, :email, class: "form-label" %>
|
<%= label f, :email, class: "form-label" %>
|
||||||
<div class="input-group has-validation mb-3">
|
<div class="input-group has-validation mb-3">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @conn, "bi-at", [class: "icon fs-5"] %>
|
<%= icon_div @conn, "bi-at", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= email_input f, :email,
|
<%= email_input f, :email,
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<%= label f, :password, class: "form-label" %>
|
<%= label f, :password, class: "form-label" %>
|
||||||
<div class="input-group has-validation mb-3">
|
<div class="input-group has-validation mb-3">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<%= icon_div @conn, "bi-lock", [class: "icon fs-5"] %>
|
<%= icon_div @conn, "bi-lock", [class: "icon"] %>
|
||||||
</span>
|
</span>
|
||||||
<%= password_input f, :password,
|
<%= password_input f, :password,
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
defmodule Bones73kWeb.LayoutView do
|
defmodule Bones73kWeb.LayoutView do
|
||||||
use Bones73kWeb, :view
|
use Bones73kWeb, :view
|
||||||
|
|
||||||
|
alias Bones73k.Accounts.User
|
||||||
|
alias Bones73kWeb.Roles
|
||||||
|
|
||||||
def nav_link_opts(conn, opts) do
|
def nav_link_opts(conn, opts) do
|
||||||
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
|
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
|
||||||
false -> opts
|
false -> opts
|
||||||
|
|
29
lib/bones73k_web/views/pagination.ex
Normal file
29
lib/bones73k_web/views/pagination.ex
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
defmodule Bones73kWeb.Pagination do
|
||||||
|
def generate_page_list(_, total_pages) when total_pages < 5,
|
||||||
|
do: 1..total_pages |> Enum.to_list()
|
||||||
|
|
||||||
|
def generate_page_list(current, total_pages),
|
||||||
|
do: first_half(1, current) ++ [current] ++ second_half(current, total_pages)
|
||||||
|
|
||||||
|
defp first_half(first, current) do
|
||||||
|
prev = current - 1
|
||||||
|
|
||||||
|
cond do
|
||||||
|
first == current -> []
|
||||||
|
prev <= first -> [first]
|
||||||
|
prev - first > 2 -> [first, -1, prev]
|
||||||
|
true -> first..prev |> Enum.to_list()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp second_half(current, last) do
|
||||||
|
next = current + 1
|
||||||
|
|
||||||
|
cond do
|
||||||
|
last == current -> []
|
||||||
|
next >= last -> [last]
|
||||||
|
last - next > 2 -> [next, -1, last]
|
||||||
|
true -> next..last |> Enum.to_list()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
6
mix.exs
6
mix.exs
|
@ -50,8 +50,10 @@ defmodule Bones73k.MixProject do
|
||||||
{:plug_cowboy, "~> 2.0"},
|
{:plug_cowboy, "~> 2.0"},
|
||||||
{:phx_gen_auth, "~> 0.5", only: [:dev], runtime: false},
|
{:phx_gen_auth, "~> 0.5", only: [:dev], runtime: false},
|
||||||
{:ecto_enum, "~> 1.4"},
|
{:ecto_enum, "~> 1.4"},
|
||||||
{:bamboo, "~> 1.7.1"},
|
{:bamboo, "~> 2.0"},
|
||||||
{:bamboo_smtp, "~> 3.1.3"}
|
{:bamboo_smtp, "~> 4.0"},
|
||||||
|
{:scrivener_ecto, "~> 2.0"},
|
||||||
|
{:timex, "~> 3.5"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
9
mix.lock
9
mix.lock
|
@ -1,8 +1,9 @@
|
||||||
%{
|
%{
|
||||||
"bamboo": {:hex, :bamboo, "1.7.1", "7f0946e8c9081ce10d347cdba33c247c7c1c4f7dddc194ab0633603ef879bbdf", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.1", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "5fb34c3ab638fc409deec47c1e91f9d78ad95bf22ccb153588b434e1ff1aa730"},
|
"bamboo": {:hex, :bamboo, "2.0.0", "0961c2727a9aa9b7ec31f0cb3711c7dcc09fe2bad4be35b7fe523148a5f65911", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "7acaccb6e63d46d7a6efa934c6bc7876c02db2b69b872731a00e03ab4d1891d8"},
|
||||||
"bamboo_smtp": {:hex, :bamboo_smtp, "3.1.3", "215edc205fe3148fd633d45914a525969cceca4b2d2cc50f165d5360951859d7", [:mix], [{:bamboo, "~> 1.7.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.1.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "b63eff97205a5bb889931f32356eaa9c5504c3495bf1211a608ab9431cfc5877"},
|
"bamboo_smtp": {:hex, :bamboo_smtp, "4.0.0", "0cc7df161d5d440d280a6d2eb20bf80bc45ea77161728a229e5ab339dcd087cd", [:mix], [{:bamboo, "~> 2.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.1.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "2412015092121b9f24f3f2e654bcd98e5c5f9afb323a94f8defa22e70ba8f23d"},
|
||||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.0", "6cb662d5c1b0a8858801cf20997bd006e7016aa8c52959c9ef80e0f34fb60b7a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2c81d61d4f6ed0e5cf7bf27a9109b791ff216a1034b3d541327484f46dd43769"},
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.0", "6cb662d5c1b0a8858801cf20997bd006e7016aa8c52959c9ef80e0f34fb60b7a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2c81d61d4f6ed0e5cf7bf27a9109b791ff216a1034b3d541327484f46dd43769"},
|
||||||
"certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"},
|
"certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"},
|
||||||
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||||
"comeonin": {:hex, :comeonin, "5.3.2", "5c2f893d05c56ae3f5e24c1b983c2d5dfb88c6d979c9287a76a7feb1e1d8d646", [:mix], [], "hexpm", "d0993402844c49539aeadb3fe46a3c9bd190f1ecf86b6f9ebd71957534c95f04"},
|
"comeonin": {:hex, :comeonin, "5.3.2", "5c2f893d05c56ae3f5e24c1b983c2d5dfb88c6d979c9287a76a7feb1e1d8d646", [:mix], [], "hexpm", "d0993402844c49539aeadb3fe46a3c9bd190f1ecf86b6f9ebd71957534c95f04"},
|
||||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||||
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
||||||
|
@ -40,10 +41,14 @@
|
||||||
"plug_crypto": {:hex, :plug_crypto, "1.2.1", "5c854427528bf61d159855cedddffc0625e2228b5f30eff76d5a4de42d896ef4", [:mix], [], "hexpm", "6961c0e17febd9d0bfa89632d391d2545d2e0eb73768f5f50305a23961d8782c"},
|
"plug_crypto": {:hex, :plug_crypto, "1.2.1", "5c854427528bf61d159855cedddffc0625e2228b5f30eff76d5a4de42d896ef4", [:mix], [], "hexpm", "6961c0e17febd9d0bfa89632d391d2545d2e0eb73768f5f50305a23961d8782c"},
|
||||||
"postgrex": {:hex, :postgrex, "0.15.8", "f5e782bbe5e8fa178d5e3cd1999c857dc48eda95f0a4d7f7bd92a50e84a0d491", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "698fbfacea34c4cf22c8281abeb5cf68d99628d541874f085520ab3b53d356fe"},
|
"postgrex": {:hex, :postgrex, "0.15.8", "f5e782bbe5e8fa178d5e3cd1999c857dc48eda95f0a4d7f7bd92a50e84a0d491", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "698fbfacea34c4cf22c8281abeb5cf68d99628d541874f085520ab3b53d356fe"},
|
||||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
||||||
|
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm", "30da36a427f2519cf75993271fb7c5aad1759682a70f90d880a85c3d743d2c57"},
|
||||||
|
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
|
||||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||||
"swoosh": {:hex, :swoosh, "1.3.2", "608819a638d527d0fd6e8892db431edd3f93c0452499880f94a80bbbcf8377e6", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "2c77c2eef273de37283bdc09c6506d466928f897600ea258fbb4765bbffd6ab2"},
|
"swoosh": {:hex, :swoosh, "1.3.2", "608819a638d527d0fd6e8892db431edd3f93c0452499880f94a80bbbcf8377e6", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "2c77c2eef273de37283bdc09c6506d466928f897600ea258fbb4765bbffd6ab2"},
|
||||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
||||||
|
"timex": {:hex, :timex, "3.6.4", "137a49450b8d1f80efff82de4b78ab9ad2e367f06346825704310599733f338b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f6fce3f07ab67f525043af5b1f68ed5fa12a41b9dab95a9a98bb6acfb30ecadc"},
|
||||||
|
"tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},
|
||||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||||
}
|
}
|
||||||
|
|
1000
priv/repo/MOCK_DATA_properties.json
Executable file
1000
priv/repo/MOCK_DATA_properties.json
Executable file
File diff suppressed because it is too large
Load diff
1000
priv/repo/MOCK_DATA_users.json
Executable file
1000
priv/repo/MOCK_DATA_users.json
Executable file
File diff suppressed because it is too large
Load diff
|
@ -9,7 +9,15 @@
|
||||||
#
|
#
|
||||||
# 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.Repo
|
||||||
alias Bones73k.Accounts
|
alias Bones73k.Accounts
|
||||||
|
alias Bones73k.Accounts.User
|
||||||
|
alias Bones73k.Properties
|
||||||
|
alias Bones73k.Properties.Property
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
## INSERTING MOCK USER DATA
|
||||||
|
|
||||||
{:ok, admin} =
|
{:ok, admin} =
|
||||||
Accounts.register_user(%{
|
Accounts.register_user(%{
|
||||||
|
@ -35,6 +43,42 @@ alias Bones73k.Accounts
|
||||||
role: Accounts.registration_role()
|
role: Accounts.registration_role()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# if Mix.env() == :dev do
|
||||||
|
this_path = Path.dirname(__ENV__.file)
|
||||||
|
|
||||||
|
users_json = Path.join(this_path, "MOCK_DATA_users.json")
|
||||||
|
|
||||||
|
count_to_take = 123
|
||||||
|
|
||||||
|
mock_users = users_json |> File.read!() |> Jason.decode!() |> Enum.take_random(count_to_take)
|
||||||
|
|
||||||
|
mock_users = ~s([
|
||||||
|
{"email":"adam@73k.us","password":"adamadam","role":"admin","inserted_at":"2018-12-14T01:01:01Z","confirmed_at":true},
|
||||||
|
{"email":"karen@73k.us","password":"karenkaren","role":"manager","inserted_at":"2018-12-14T01:06:01Z","confirmed_at":true},
|
||||||
|
{"email":"kat@73k.us","password":"katkat","role":"manager","inserted_at":"2018-12-14T01:06:01Z","confirmed_at":true}
|
||||||
|
]) |> Jason.decode!() |> Enum.concat(mock_users)
|
||||||
|
|
||||||
|
mock_users =
|
||||||
|
Enum.map(mock_users, fn e ->
|
||||||
|
add_dt = NaiveDateTime.from_iso8601!(e["inserted_at"])
|
||||||
|
|
||||||
|
%{
|
||||||
|
email: e["email"],
|
||||||
|
role: String.to_existing_atom(e["role"]),
|
||||||
|
hashed_password: Bcrypt.hash_pwd_salt(e["password"]),
|
||||||
|
inserted_at: add_dt,
|
||||||
|
updated_at: add_dt,
|
||||||
|
confirmed_at: (e["confirmed_at"] && NaiveDateTime.add(add_dt, 300, :second)) || nil
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Repo.insert_all(User, mock_users)
|
||||||
|
# end
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
## IF ENV IS DEV
|
||||||
|
## INSERTING MOCK PROPERTIES DATA
|
||||||
|
|
||||||
Enum.each(1..10, fn i ->
|
Enum.each(1..10, fn i ->
|
||||||
%{
|
%{
|
||||||
name: "Property #{i} - User 1",
|
name: "Property #{i} - User 1",
|
||||||
|
@ -60,3 +104,29 @@ Enum.each(1..10, fn i ->
|
||||||
}
|
}
|
||||||
|> Bones73k.Properties.create_property()
|
|> Bones73k.Properties.create_property()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
# if Mix.env() == :dev do
|
||||||
|
# this_path = Path.dirname(__ENV__.file)
|
||||||
|
|
||||||
|
props_json = Path.join(this_path, "MOCK_DATA_properties.json")
|
||||||
|
|
||||||
|
count_to_take = 123
|
||||||
|
|
||||||
|
mock_props = props_json |> File.read!() |> Jason.decode!() |> Enum.take_random(count_to_take)
|
||||||
|
|
||||||
|
mock_props =
|
||||||
|
Enum.map(mock_props, fn e ->
|
||||||
|
add_dt = NaiveDateTime.from_iso8601!(e["inserted_at"])
|
||||||
|
|
||||||
|
%{
|
||||||
|
name: e["name"],
|
||||||
|
price: e["price"],
|
||||||
|
description: e["description"],
|
||||||
|
user_id: e["user_id"],
|
||||||
|
inserted_at: add_dt,
|
||||||
|
updated_at: add_dt
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Repo.insert_all(Property, mock_props)
|
||||||
|
# end
|
||||||
|
|
Loading…
Reference in a new issue