working to get bones73k up to par with work done on shift73k

This commit is contained in:
Adam Piontek 2021-03-26 18:55:17 -04:00
parent 19f14f7046
commit 2e61ee0031
14 changed files with 73 additions and 134 deletions

3
.gitignore vendored
View File

@ -47,3 +47,6 @@ npm-debug.log
# for vscode elixir_ls extension files # for vscode elixir_ls extension files
/.elixir_ls /.elixir_ls
# dev
TODO.md

View File

@ -7,6 +7,8 @@
# General application configuration # General application configuration
use Mix.Config use Mix.Config
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
config :bones73k, config :bones73k,
ecto_repos: [Bones73k.Repo] ecto_repos: [Bones73k.Repo]

View File

@ -1,44 +0,0 @@
# In this file, we load production configuration and secrets
# from environment variables. You can also hardcode secrets,
# although such is generally not recommended and you have to
# remember to add this file to your .gitignore.
use Mix.Config
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
config :bones73k, Bones73k.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""
config :bones73k, Bones73kWeb.Endpoint,
http: [
port: String.to_integer(System.get_env("PORT") || "4000"),
transport_options: [socket_opts: [:inet6]]
],
secret_key_base: secret_key_base
# ## Using releases (Elixir v1.9+)
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start each relevant endpoint:
#
# config :bones73k, Bones73kWeb.Endpoint, server: true
#
# Then you can assemble a release by calling `mix release`.
# See `mix help release` for more information.
# Import extra secret stuff not to be included in git repo
import_config "really.secret.exs"

View File

@ -6,4 +6,9 @@ defmodule Bones73k do
Contexts are also responsible for managing your data, regardless Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others. if it comes from the database, an external API or others.
""" """
@app_vars Application.get_env(:bones73k, :app_global_vars, time_zone: "America/New_York")
@app_time_zone @app_vars[:time_zone]
def app_time_zone, do: @app_time_zone
end end

View File

@ -1,7 +1,6 @@
defmodule Bones73k.Accounts.User do defmodule Bones73k.Accounts.User do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import EctoEnum
@roles [ @roles [
user: "Basic user level", user: "Basic user level",
@ -9,8 +8,6 @@ defmodule Bones73k.Accounts.User do
admin: "Can delete users and change user roles" 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
@max_password 80 @max_password 80
@ -23,8 +20,8 @@ defmodule Bones73k.Accounts.User do
field :password, :string, virtual: true field :password, :string, virtual: true
field :hashed_password, :string field :hashed_password, :string
field :confirmed_at, :naive_datetime field :confirmed_at, :naive_datetime
field :role, Ecto.Enum, values: Keyword.keys(@roles), default: :user
field :role, RolesEnum, default: :user
timestamps() timestamps()
end end
@ -73,21 +70,10 @@ defmodule Bones73k.Accounts.User do
|> validate_password_not_required(opts) |> validate_password_not_required(opts)
end 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
(RolesEnum.valid_value?(role) && []) || [role: "invalid user role"]
end
defp validate_role(changeset) do defp validate_role(changeset) do
changeset changeset
|> validate_required([:role]) |> validate_required([:role])
|> validate_change(:role, &role_validator/2) |> validate_inclusion(:role, Keyword.keys(@roles), message: "invalid user role")
end end
defp validate_email_format(changeset) do defp validate_email_format(changeset) do
@ -115,9 +101,11 @@ defmodule Bones73k.Accounts.User do
defp validate_password_not_required(changeset, opts) do defp validate_password_not_required(changeset, opts) do
changeset 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")
# |> 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"
)
|> maybe_hash_password(opts) |> maybe_hash_password(opts)
end end

View File

@ -15,9 +15,9 @@ defmodule Bones73k.Accounts.UserToken do
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id @foreign_key_type :binary_id
schema "users_tokens" do schema "users_tokens" do
field :token, :binary field(:token, :binary)
field :context, :string field(:context, :string)
field :sent_to, :string field(:sent_to, :string)
belongs_to :user, Bones73k.Accounts.User belongs_to :user, Bones73k.Accounts.User
timestamps(updated_at: false) timestamps(updated_at: false)
@ -40,10 +40,11 @@ defmodule Bones73k.Accounts.UserToken do
""" """
def verify_session_token_query(token) do def verify_session_token_query(token) do
query = query =
from token in token_and_context_query(token, "session"), from(token in token_and_context_query(token, "session"),
join: user in assoc(token, :user), join: user in assoc(token, :user),
where: token.inserted_at > ago(@session_validity_in_days, "day"), where: token.inserted_at > ago(@session_validity_in_days, "day"),
select: user select: user
)
{:ok, query} {:ok, query}
end end
@ -85,10 +86,11 @@ defmodule Bones73k.Accounts.UserToken do
days = days_for_context(context) days = days_for_context(context)
query = query =
from token in token_and_context_query(hashed_token, context), from(token in token_and_context_query(hashed_token, context),
join: user in assoc(token, :user), join: user in assoc(token, :user),
where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email, where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email,
select: user select: user
)
{:ok, query} {:ok, query}
@ -111,8 +113,9 @@ defmodule Bones73k.Accounts.UserToken do
hashed_token = :crypto.hash(@hash_algorithm, decoded_token) hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
query = query =
from token in token_and_context_query(hashed_token, context), from(token in token_and_context_query(hashed_token, context),
where: token.inserted_at > ago(@change_email_validity_in_days, "day") where: token.inserted_at > ago(@change_email_validity_in_days, "day")
)
{:ok, query} {:ok, query}
@ -125,17 +128,17 @@ defmodule Bones73k.Accounts.UserToken do
Returns the given token with the given context. Returns the given token with the given context.
""" """
def token_and_context_query(token, context) do def token_and_context_query(token, context) do
from Bones73k.Accounts.UserToken, where: [token: ^token, context: ^context] from(Bones73k.Accounts.UserToken, where: [token: ^token, context: ^context])
end end
@doc """ @doc """
Gets all tokens for the given user for the given contexts. Gets all tokens for the given user for the given contexts.
""" """
def user_and_contexts_query(user, :all) do def user_and_contexts_query(user, :all) do
from t in Bones73k.Accounts.UserToken, where: t.user_id == ^user.id from(t in Bones73k.Accounts.UserToken, where: t.user_id == ^user.id)
end end
def user_and_contexts_query(user, [_ | _] = contexts) do def user_and_contexts_query(user, [_ | _] = contexts) do
from t in Bones73k.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts from(t in Bones73k.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts)
end end
end end

View File

@ -1,10 +0,0 @@
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

View File

@ -3,7 +3,6 @@ defmodule Bones73kWeb.UserManagementLive.Index do
import Ecto.Query import Ecto.Query
import Bones73kWeb.Pagination import Bones73kWeb.Pagination
import Bones73k.Util.Dt
alias Bones73k.Repo alias Bones73k.Repo
alias Bones73k.Accounts alias Bones73k.Accounts
@ -27,9 +26,8 @@ defmodule Bones73kWeb.UserManagementLive.Index do
if Roles.can?(current_user, user, live_action) do if Roles.can?(current_user, user, live_action) do
socket socket
|> assign(:query, query_map(params)) |> assign(:query, query_map(params))
|> assign_modal_return_to() |> assign_modal_close_handlers()
|> assign(:delete_user, nil) |> assign(:delete_user, nil)
|> assign(:page, nil)
|> request_page_query() |> request_page_query()
|> apply_action(socket.assigns.live_action, params) |> apply_action(socket.assigns.live_action, params)
|> live_noreply() |> live_noreply()
@ -64,9 +62,9 @@ defmodule Bones73kWeb.UserManagementLive.Index do
|> assign(:user, Accounts.get_user!(id)) |> assign(:user, Accounts.get_user!(id))
end end
def assign_modal_return_to(%{assigns: %{query: query}} = socket) do defp assign_modal_close_handlers(%{assigns: %{query: query}} = socket) do
to = Routes.user_management_index_path(socket, :index, Enum.into(query, [])) to = Routes.user_management_index_path(socket, :index, Enum.into(query, []))
assign(socket, :modal_return_to, to) assign(socket, modal_return_to: to, modal_close_action: :return)
end end
defp user_from_params(params) defp user_from_params(params)
@ -97,7 +95,10 @@ defmodule Bones73kWeb.UserManagementLive.Index do
@impl true @impl true
def handle_event("delete-modal", %{"id" => id}, socket) do def handle_event("delete-modal", %{"id" => id}, socket) do
{:noreply, assign(socket, :delete_user, Accounts.get_user(id))} socket
|> assign(:modal_close_action, :delete_user)
|> assign(:delete_user, Accounts.get_user!(id))
|> live_noreply()
end end
@impl true @impl true
@ -113,17 +114,13 @@ defmodule Bones73kWeb.UserManagementLive.Index do
end end
@impl true @impl true
def handle_event( def handle_event("sort-change", %{"sort_by" => column} = params, socket) do
"sort-change", if column == socket.assigns.query.sort_by do
%{"sort_by" => column} = params, order = (socket.assigns.query.sort_order == "asc" && "desc") || "asc"
%{assigns: %{query: query}} = socket send(self(), {:query_update, %{"sort_order" => order}})
) do else
(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")}) send(self(), {:query_update, Map.put(params, "sort_order", "asc")})
end
{:noreply, socket} {:noreply, socket}
end end
@ -168,8 +165,20 @@ defmodule Bones73kWeb.UserManagementLive.Index do
end end
@impl true @impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: :return}} = socket) do
socket |> copy_flash() |> push_patch(to: to) |> live_noreply() socket
|> copy_flash()
|> push_patch(to: socket.assigns.modal_return_to)
|> live_noreply()
end
@impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_close_action: assign_key}} = socket) do
socket
|> assign(assign_key, nil)
|> assign_modal_close_handlers()
|> request_page_query()
|> live_noreply()
end end
@impl true @impl true
@ -187,5 +196,9 @@ defmodule Bones73kWeb.UserManagementLive.Index do
] ]
end end
def dt_out(ndt), do: format_ndt(ndt, "{YYYY} {Mshort} {0D}, {h12}:{0m} {am}") def dt_out(ndt) do
ndt
|> DateTime.from_naive!(Bones73k.app_time_zone())
|> Calendar.strftime("%Y %b %-d, %-I:%M %p")
end
end end

View File

@ -11,8 +11,8 @@
<%= live_modal @socket, Bones73kWeb.UserManagement.DeleteComponent, <%= live_modal @socket, Bones73kWeb.UserManagement.DeleteComponent,
id: @delete_user.id, id: @delete_user.id,
title: "Delete User", title: "Delete User",
delete_user: @delete_user, delete_user: @delete_user
current_user: @current_user %> %>
<% end %> <% end %>
@ -92,20 +92,20 @@
</dt> </dt>
<dd class="d-inline d-sm-block col-auto"> <dd class="d-inline d-sm-block col-auto">
<span class="visually-hidden"><%= user.confirmed_at && "Yes" || "No" %></span> <span class="visually-hidden"><%= user.confirmed_at && "Yes" || "No" %></span>
<input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %>> <input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
</dd> </dd>
</dl> </dl>
<%= if Roles.can?(@current_user, user, :edit) do %> <%= if Roles.can?(@current_user, user, :edit) do %>
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-primary btn-sm text-nowrap" do %> <%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-primary btn-sm text-nowrap" do %>
<%= icon_div @socket, "bi-pencil", [class: "icon baseline", style: "margin-right:0.125rem;"] %> <%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
Edit Edit
<% end %> <% end %>
<% end %> <% end %>
<%= if Roles.can?(@current_user, user, :delete) do %> <%= if Roles.can?(@current_user, user, :delete) do %>
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>"> <button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>">
<%= icon_div @socket, "bi-trash", [class: "icon baseline", style: "margin-right:0.125rem;"] %> <%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
Delete Delete
</button> </button>
<% end %> <% end %>
@ -174,20 +174,20 @@
<td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td> <td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td>
<td class="align-middle"> <td class="align-middle">
<span class="visually-hidden"><%= user.confirmed_at && "Confirmed" || "Not confirmed" %></span> <span class="visually-hidden"><%= user.confirmed_at && "Confirmed" || "Not confirmed" %></span>
<input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %>> <input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
</td> </td>
<td class="align-middle text-end text-nowrap"> <td class="align-middle text-end text-nowrap">
<%= if Roles.can?(@current_user, user, :edit) do %> <%= if Roles.can?(@current_user, user, :edit) do %>
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm text-nowrap" do %> <%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
<%= icon_div @socket, "bi-pencil", [class: "icon baseline", style: "margin-right:0.125rem;"] %> <%= icon_div @socket, "bi-pencil", [class: "icon baseline"] %>
Edit Edit
<% end %> <% end %>
<% end %> <% end %>
<%= if Roles.can?(@current_user, user, :delete) do %> <%= if Roles.can?(@current_user, user, :delete) do %>
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>"> <button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>">
<%= icon_div @socket, "bi-trash", [class: "icon baseline", style: "margin-right:0.125rem;"] %> <%= icon_div @socket, "bi-trash", [class: "icon baseline"] %>
Delete Delete
</button> </button>
<% end %> <% end %>

View File

@ -1,7 +1,7 @@
<%= if !@current_user do %> <%= if !@current_user do %>
<%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light") do %> <%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "btn btn-outline-light") do %>
<%= icon_div @conn, "bi-door-open", [class: "icon baseline", style: "margin-right:0.125rem;"] %> <%= icon_div @conn, "bi-door-open", [class: "icon baseline"] %>
Log in Log in
<% end %> <% end %>

View File

@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "Bones73k", suffix: " · Phoenix Framework" %> <%= live_title_tag assigns[:page_title] || "", suffix: assigns[:page_title] && " · Bones73k" || "Bones73k" %>
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/> <link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script> <script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head> </head>

View File

@ -48,11 +48,10 @@ defmodule Bones73k.MixProject do
{:gettext, "~> 0.11"}, {:gettext, "~> 0.11"},
{:jason, "~> 1.0"}, {:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"}, {:plug_cowboy, "~> 2.0"},
{:ecto_enum, "~> 1.4"},
{:bamboo, "~> 2.0"}, {:bamboo, "~> 2.0"},
{:bamboo_smtp, "~> 4.0"}, {:bamboo_smtp, "~> 4.0"},
{:scrivener_ecto, "~> 2.0"}, {:scrivener_ecto, "~> 2.0"},
{:timex, "~> 3.5"} {:tzdata, "~> 1.1"}
] ]
end end

View File

@ -8,6 +8,7 @@ defmodule Bones73k.Repo.Migrations.CreateUsersAuthTables do
add(:id, :binary_id, primary_key: true) add(:id, :binary_id, primary_key: true)
add(:email, :citext, null: false) add(:email, :citext, null: false)
add(:hashed_password, :string, null: false) add(:hashed_password, :string, null: false)
add(:role, :string, null: false)
add(:confirmed_at, :naive_datetime) add(:confirmed_at, :naive_datetime)
timestamps() timestamps()
end end

View File

@ -1,21 +0,0 @@
defmodule Bones73k.Repo.Migrations.AddRoleToUsers do
use Ecto.Migration
alias Bones73k.Accounts.User.RolesEnum
def up do
RolesEnum.create_type()
alter table(:users) do
add(:role, RolesEnum.type(), null: false)
end
end
def down do
alter table(:users) do
remove(:role)
end
RolesEnum.drop_type()
end
end