Compare commits

...

3 commits

Author SHA1 Message Date
339d075edd
feat: add basic admin mode 2025-02-22 10:39:05 -05:00
56730540b7
add placeholder admin page 2025-02-22 07:22:28 -05:00
dee1bef5bf
chore: install and run styler 2025-02-22 07:22:20 -05:00
19 changed files with 295 additions and 107 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
export PASSWORD_HASH='$argon2id$v=19$m=65536,t=3,p=4$ctxMPSfgu5i28J0bjRl2yg$D7+qs+R7caAe5lw5m7s+k9M0t75R4XBhkwG1dv6MGOQ'

View file

@ -1,6 +1,6 @@
[ [
import_deps: [:ecto, :ecto_sql, :phoenix], import_deps: [:ecto, :ecto_sql, :phoenix],
subdirectories: ["priv/*/migrations"], subdirectories: ["priv/*/migrations"],
plugins: [Phoenix.LiveView.HTMLFormatter], plugins: [Styler, Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
] ]

View file

@ -7,11 +7,6 @@
# General application configuration # General application configuration
import Config import Config
config :cms,
namespace: CMS,
ecto_repos: [CMS.Repo],
generators: [timestamp_type: :utc_datetime, binary_id: true]
# Configures the endpoint # Configures the endpoint
config :cms, CMSWeb.Endpoint, config :cms, CMSWeb.Endpoint,
url: [host: "localhost"], url: [host: "localhost"],
@ -23,28 +18,20 @@ config :cms, CMSWeb.Endpoint,
pubsub_server: CMS.PubSub, pubsub_server: CMS.PubSub,
live_view: [signing_salt: "afQxdsCJ"] live_view: [signing_salt: "afQxdsCJ"]
config :cms,
namespace: CMS,
ecto_repos: [CMS.Repo],
generators: [timestamp_type: :utc_datetime, binary_id: true]
# Configure esbuild (the version is required) # Configure esbuild (the version is required)
config :esbuild, config :esbuild,
version: "0.17.11", version: "0.17.11",
cms: [ cms: [
args: args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__), cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
] ]
# Configure tailwind (the version is required)
config :tailwind,
version: "3.4.3",
cms: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]
# Configures Elixir's Logger # Configures Elixir's Logger
config :logger, :console, config :logger, :console,
format: "$time $metadata[$level] $message\n", format: "$time $metadata[$level] $message\n",
@ -53,6 +40,19 @@ config :logger, :console,
# Use Jason for JSON parsing in Phoenix # Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason config :phoenix, :json_library, Jason
# Import environment specific config. This must remain at the bottom # Configure tailwind (the version is required)
# of this file so it overrides the configuration defined above. config :tailwind,
version: "3.4.3",
cms: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/static/assets/app.css
),
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
cd: Path.expand("../assets", __DIR__)
]
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"

View file

@ -1,6 +1,13 @@
import Config import Config
# Configure your database # Configure your database
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we can use it
# to bundle .js and .css sources.
# Binding to loopback ipv4 address prevents access from other machines.
config :cms, CMS.Repo, config :cms, CMS.Repo,
username: "postgres", username: "postgres",
password: "postgres", password: "postgres",
@ -10,13 +17,6 @@ config :cms, CMS.Repo,
show_sensitive_data_on_connection_error: true, show_sensitive_data_on_connection_error: true,
pool_size: 10 pool_size: 10
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we can use it
# to bundle .js and .css sources.
# Binding to loopback ipv4 address prevents access from other machines.
config :cms, CMSWeb.Endpoint, config :cms, CMSWeb.Endpoint,
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
http: [ip: {127, 0, 0, 1}, port: 4000], http: [ip: {127, 0, 0, 1}, port: 4000],
@ -29,29 +29,6 @@ config :cms, CMSWeb.Endpoint,
tailwind: {Tailwind, :install_and_run, [:cms, ~w(--watch)]} tailwind: {Tailwind, :install_and_run, [:cms, ~w(--watch)]}
] ]
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# Mix task:
#
# mix phx.gen.cert
#
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
#
# https: [
# port: 4001,
# cipher_suite: :strong,
# keyfile: "priv/cert/selfsigned_key.pem",
# certfile: "priv/cert/selfsigned.pem"
# ],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.
# Watch static and templates for browser reloading. # Watch static and templates for browser reloading.
config :cms, CMSWeb.Endpoint, config :cms, CMSWeb.Endpoint,
live_reload: [ live_reload: [
@ -61,21 +38,45 @@ config :cms, CMSWeb.Endpoint,
] ]
] ]
# ## SSL Support
#
# Enable dev routes for dashboard and mailbox # Enable dev routes for dashboard and mailbox
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# Mix task:
#
config :cms, dev_routes: true config :cms, dev_routes: true
# Do not include metadata nor timestamps in development logs # Do not include metadata nor timestamps in development logs
# mix phx.gen.cert
#
# Run `mix help phx.gen.cert` for more information.
#
config :logger, :console, format: "[$level] $message\n" config :logger, :console, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation # Initialize plugs at runtime for faster development compilation
# The `http:` config above can be replaced with:
#
# https: [
config :phoenix, :plug_init_mode, :runtime config :phoenix, :plug_init_mode, :runtime
# Set a higher stacktrace during development. Avoid configuring such
# port: 4001,
# in production as building large stacktraces may be expensive.
# cipher_suite: :strong,
# keyfile: "priv/cert/selfsigned_key.pem",
# certfile: "priv/cert/selfsigned.pem"
config :phoenix, :stacktrace_depth, 20
config :phoenix_live_view, config :phoenix_live_view,
# Include HEEx debug annotations as HTML comments in rendered markup # Include HEEx debug annotations as HTML comments in rendered markup
# ],
#
debug_heex_annotations: true, debug_heex_annotations: true,
# Enable helpful, but potentially expensive runtime checks # Enable helpful, but potentially expensive runtime checks
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.
enable_expensive_runtime_checks: true enable_expensive_runtime_checks: true

View file

@ -20,6 +20,15 @@ if System.get_env("PHX_SERVER") do
config :cms, CMSWeb.Endpoint, server: true config :cms, CMSWeb.Endpoint, server: true
end end
config :cms,
password_hash:
System.get_env("PASSWORD_HASH") ||
raise("""
environment variable PASSWORD_HASH is missing.
Generate a hashed password using `mix cms.gen.password_hash`
""")
if config_env() == :prod do if config_env() == :prod do
database_url = database_url =
System.get_env("DATABASE_URL") || System.get_env("DATABASE_URL") ||
@ -30,12 +39,6 @@ if config_env() == :prod do
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
config :cms, CMS.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6
# The secret key base is used to sign/encrypt cookies and other secrets. # The secret key base is used to sign/encrypt cookies and other secrets.
# A default value is used in config/dev.exs and config/test.exs but you # A default value is used in config/dev.exs and config/test.exs but you
# want to use a different value for prod and you most likely don't want # want to use a different value for prod and you most likely don't want
@ -51,7 +54,11 @@ if config_env() == :prod do
host = System.get_env("PHX_HOST") || "example.com" host = System.get_env("PHX_HOST") || "example.com"
port = String.to_integer(System.get_env("PORT") || "4000") port = String.to_integer(System.get_env("PORT") || "4000")
config :cms, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") config :cms, CMS.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6
config :cms, CMSWeb.Endpoint, config :cms, CMSWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"], url: [host: host, port: 443, scheme: "https"],
@ -65,6 +72,8 @@ if config_env() == :prod do
], ],
secret_key_base: secret_key_base secret_key_base: secret_key_base
config :cms, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
# ## SSL Support # ## SSL Support
# #
# To get SSL working, you will need to add the `https` key # To get SSL working, you will need to add the `https` key

View file

@ -23,10 +23,11 @@ defmodule CMSWeb do
quote do quote do
use Phoenix.Router, helpers: false use Phoenix.Router, helpers: false
# Import common connection and controller functions to use in pipelines
import Plug.Conn
import Phoenix.Controller import Phoenix.Controller
import Phoenix.LiveView.Router import Phoenix.LiveView.Router
# Import common connection and controller functions to use in pipelines
import Plug.Conn
end end
end end
@ -80,10 +81,10 @@ defmodule CMSWeb do
defp html_helpers do defp html_helpers do
quote do quote do
import CMSWeb.CoreComponents
# HTML escaping functionality # HTML escaping functionality
import Phoenix.HTML import Phoenix.HTML
# Core UI components # Core UI components
import CMSWeb.CoreComponents
# Shortcut for generating JS commands # Shortcut for generating JS commands
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS

View file

@ -1,14 +1,30 @@
<div class="flex flex-col md:flex-row mx-auto max-w-3xl"> <div :if={@admin?} class="flex flex-row justify-between py-1 px-3 mb-2 border-b border-slate-100">
<section class="flex flex-col p-2 gap-y-1 border-slate-100 border-b md:border-b-0"> <section class="flex flex-row gap-x-2">
<.link navigate={~p"/"} class="font-bold hover:underline">sloanelybutsurely.com</.link> <div class="pr-2 border-r border-slate-100">
<.link navigate={~p"/admin"} class="font-bold">admin mode</.link>
</div>
<nav> <nav>
<ul> <ul class="flex flex-row">
<li><.link navigate={~p"/writing"} class="hover:underline">writing</.link></li> <.link href="#" class="hover:underline">new post</.link>
<li><.link navigate={~p"/microblog"} class="hover:underline">microblog</.link></li>
</ul> </ul>
</nav> </nav>
</section> </section>
<main>
<section class="flex flex-row">
<.link href={~p"/admin/session"} method="delete" class="hover:underline">sign out</.link>
</section>
</div>
<div class="flex flex-col md:flex-row mx-auto max-w-4xl">
<section class="flex flex-col p-2 gap-y-1 border-slate-100 border-b md:border-b-0">
<.link href={~p"/"} class="font-bold hover:underline">sloanelybutsurely.com</.link>
<nav>
<ul>
<li><.link href={~p"/writing"} class="hover:underline">writing</.link></li>
<li><.link href={~p"/microblog"} class="hover:underline">microblog</.link></li>
</ul>
</nav>
</section>
<main class="p-2">
{@inner_content} {@inner_content}
</main> </main>
</div> </div>

View file

@ -0,0 +1,40 @@
defmodule CMSWeb.AdminAuth do
@moduledoc false
use CMSWeb, :verified_routes
import Phoenix.Controller
import Plug.Conn
def log_in_admin(conn) do
conn
|> renew_session()
|> put_session(:admin?, true)
|> redirect(to: ~p"/")
end
def log_out_admin(conn) do
if live_socket_id = get_session(conn, :live_socket_id) do
CMSWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
|> renew_session()
|> redirect(to: ~p"/")
end
def correct_password?(password) do
password_hash = Application.fetch_env!(:cms, :password_hash)
Argon2.verify_pass(password, password_hash)
end
## private
defp renew_session(conn) do
delete_csrf_token()
conn
|> configure_session(renew: true)
|> clear_session()
end
end

View file

@ -0,0 +1,18 @@
defmodule CMSWeb.AdminMode do
@moduledoc false
use CMSWeb, :live_view
def admin_mode(%Plug.Conn{} = conn, _opts) do
Plug.Conn.assign(conn, :admin?, admin?(conn))
end
def on_mount(:default, _params, session, socket) do
{:cont, assign(socket, :admin?, admin?(session))}
end
defp admin?(%Plug.Conn{} = conn) do
Plug.Conn.get_session(conn, :admin?, false) == true
end
defp admin?(%{} = session), do: Map.get(session, :admin?, false) == true
end

View file

@ -0,0 +1,23 @@
defmodule CMSWeb.AdminSessionController do
use CMSWeb, :controller
alias CMSWeb.AdminAuth
def create(conn, %{"password" => password}) do
if AdminAuth.correct_password?(password) do
AdminAuth.log_in_admin(conn)
else
redirect(conn, to: ~p"/admin/sign-in")
end
end
def create(conn, _params) do
redirect(conn, to: ~p"/admin/sign-in")
end
def destroy(conn, _params) do
conn
|> AdminAuth.log_out_admin()
|> redirect(to: ~p"/")
end
end

View file

@ -0,0 +1,16 @@
defmodule CMSWeb.AdminLive do
@moduledoc false
use CMSWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<h1>AdminLive</h1>
"""
end
end

View file

@ -0,0 +1,29 @@
defmodule CMSWeb.AdminLoginLive do
@moduledoc false
use CMSWeb, :live_view
@impl true
def mount(_params, _session, socket) do
socket = assign(socket, :form, to_form(%{"password" => ""}))
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<h1 class="font-bold text-lg mb-4">Sign in</h1>
<.form for={@form} action={~p"/admin/session"}>
<input
type="password"
placeholder="password"
id={@form[:password].id}
name={@form[:password].name}
value={@form[:password].value}
required
/>
</.form>
"""
end
end

View file

@ -1,6 +1,10 @@
defmodule CMSWeb.Router do defmodule CMSWeb.Router do
use CMSWeb, :router use CMSWeb, :router
import CMSWeb.AdminMode
alias CMSWeb.AdminMode
pipeline :browser do pipeline :browser do
plug :accepts, ["html"] plug :accepts, ["html"]
plug :fetch_session plug :fetch_session
@ -8,25 +12,26 @@ defmodule CMSWeb.Router do
plug :put_root_layout, html: {CMSWeb.Layouts, :root} plug :put_root_layout, html: {CMSWeb.Layouts, :root}
plug :protect_from_forgery plug :protect_from_forgery
plug :put_secure_browser_headers plug :put_secure_browser_headers
plug :admin_mode
end end
pipeline :api do live_session :default, on_mount: AdminMode do
plug :accepts, ["json"] scope "/", CMSWeb do
pipe_through :browser
get "/", PageController, :home
get "/writing", PageController, :writing
get "/microblog", PageController, :microblog
live "/admin", AdminLive
live "/admin/sign-in", AdminLoginLive
post "/admin/session", AdminSessionController, :create
delete "/admin/session", AdminSessionController, :destroy
end
end end
scope "/", CMSWeb do
pipe_through :browser
get "/", PageController, :home
get "/writing", PageController, :writing
get "/microblog", PageController, :microblog
end
# Other scopes may use custom stacks.
# scope "/api", CMSWeb do
# pipe_through :api
# end
# Enable LiveDashboard in development # Enable LiveDashboard in development
if Application.compile_env(:cms, :dev_routes) do if Application.compile_env(:cms, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put # If you want to use the LiveDashboard in production, you should put

View file

@ -1,5 +1,7 @@
defmodule CMSWeb.Telemetry do defmodule CMSWeb.Telemetry do
@moduledoc false
use Supervisor use Supervisor
import Telemetry.Metrics import Telemetry.Metrics
def start_link(arg) do def start_link(arg) do
@ -70,8 +72,7 @@ defmodule CMSWeb.Telemetry do
), ),
summary("cms.repo.query.idle_time", summary("cms.repo.query.idle_time",
unit: {:native, :millisecond}, unit: {:native, :millisecond},
description: description: "The time the connection spent waiting before being checked out for the query"
"The time the connection spent waiting before being checked out for the query"
), ),
# VM Metrics # VM Metrics

View file

@ -0,0 +1,23 @@
defmodule Mix.Tasks.Cms.Gen.PasswordHash do
@shortdoc @moduledoc
@moduledoc """
Hashes a password for the admin account
"""
use Mix.Task
@impl Mix.Task
def run(_args) do
password = Mix.shell().prompt("Password: ")
password = String.trim_trailing(password)
password_confirmation = Mix.shell().prompt("Confirm password: ")
password_confirmation = String.trim_trailing(password_confirmation)
if password == password_confirmation do
hashed = Argon2.hash_pwd_salt(password)
Mix.shell().info(hashed)
else
Mix.shell().error("Passwords do not match")
end
end
end

13
mix.exs
View file

@ -44,17 +44,16 @@ defmodule CMS.MixProject do
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
{:heroicons, {:heroicons,
github: "tailwindlabs/heroicons", github: "tailwindlabs/heroicons", tag: "v2.1.1", sparse: "optimized", app: false, compile: false, depth: 1},
tag: "v2.1.1",
sparse: "optimized",
app: false,
compile: false,
depth: 1},
{:telemetry_metrics, "~> 1.0"}, {:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"}, {:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"}, {:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.5"} {:bandit, "~> 1.5"},
{:argon2_elixir, "~> 4.1"},
# dev/test only
{:styler, "~> 1.4", only: [:dev, :test], runtime: false}
] ]
end end

View file

@ -1,11 +1,14 @@
%{ %{
"argon2_elixir": {:hex, :argon2_elixir, "4.1.2", "1160a3ccd59b951175525882240651f5ed3303b75c616204713f8b31c76b37bd", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9222341e1b0d9aa5ca7e26a1c77bd1bd92d2314c92b57ca3e2c7ed847223b51d"},
"bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"}, "bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"},
"castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"}, "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
@ -24,6 +27,7 @@
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{: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]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{: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]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
"styler": {:hex, :styler, "1.4.0", "5944723d08afe4d38210b674d7e97dd1137a75968a85a633983cc308e86dc5f2", [:mix], [], "hexpm", "07de0e89c27490c8e469bb814d77ddaaa3283d7d8038501021d80a7705cf13e9"},
"tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},

View file

@ -19,15 +19,15 @@ defmodule CMSWeb.ConnCase do
using do using do
quote do quote do
use CMSWeb, :verified_routes
import CMSWeb.ConnCase
import Phoenix.ConnTest
import Plug.Conn
# The default endpoint for testing # The default endpoint for testing
@endpoint CMSWeb.Endpoint @endpoint CMSWeb.Endpoint
use CMSWeb, :verified_routes
# Import conveniences for testing with connections # Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import CMSWeb.ConnCase
end end
end end

View file

@ -16,14 +16,16 @@ defmodule CMS.DataCase do
use ExUnit.CaseTemplate use ExUnit.CaseTemplate
alias Ecto.Adapters.SQL.Sandbox
using do using do
quote do quote do
alias CMS.Repo import CMS.DataCase
import Ecto import Ecto
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
import CMS.DataCase
alias CMS.Repo
end end
end end
@ -36,8 +38,8 @@ defmodule CMS.DataCase do
Sets up the sandbox based on the test tags. Sets up the sandbox based on the test tags.
""" """
def setup_sandbox(tags) do def setup_sandbox(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(CMS.Repo, shared: not tags[:async]) pid = Sandbox.start_owner!(CMS.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) on_exit(fn -> Sandbox.stop_owner(pid) end)
end end
@doc """ @doc """