chore: reorganize project, enforce boundaries

This commit is contained in:
sloane 2025-03-22 10:45:15 -04:00
parent 41ad5aa924
commit fae90f44da
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
69 changed files with 369 additions and 638 deletions

View file

@ -8,13 +8,13 @@ const path = require("path")
module.exports = {
content: [
"./js/**/*.js",
"../lib/cms_web.ex",
"../lib/cms_web/**/*.*ex"
"../lib/web.ex",
"../lib/web/**/*.*ex"
],
theme: {
extend: {
typography: ({theme}) => ({
cms: {
typography: ({ theme }) => ({
sloanely_but_surely: {
css: {
'--tw-prose-body': theme('colors.black'),
'--tw-prose-headings': theme('colors.black'),
@ -61,14 +61,14 @@ module.exports = {
//
// <div class="phx-click-loading:animate-ping">
//
plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
plugin(({ addVariant }) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
plugin(({ addVariant }) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
plugin(({ addVariant }) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
// See your `CoreComponents.icon/1` for more information.
//
plugin(function({matchComponents, theme}) {
plugin(function({ matchComponents, theme }) {
let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
let values = {}
let icons = [
@ -80,11 +80,11 @@ module.exports = {
icons.forEach(([suffix, dir]) => {
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
let name = path.basename(file, ".svg") + suffix
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
values[name] = { name, fullPath: path.join(iconsDir, dir, file) }
})
})
matchComponents({
"hero": ({name, fullPath}) => {
"hero": ({ name, fullPath }) => {
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
let size = theme("spacing.6")
if (name.endsWith("-mini")) {
@ -104,7 +104,7 @@ module.exports = {
"height": size
}
}
}, {values})
}, { values })
})
]
}

View file

@ -1,57 +1,42 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
# General application configuration
import Config
# Configures the endpoint
config :cms, CMSWeb.Endpoint,
url: [host: "localhost"],
adapter: Bandit.PhoenixAdapter,
render_errors: [
formats: [html: CMSWeb.ErrorHTML, json: CMSWeb.ErrorJSON],
layout: false
],
pubsub_server: CMS.PubSub,
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)
config :esbuild,
version: "0.17.11",
cms: [
sloanely_but_surely: [
args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
# Configure tailwind (the version is required)
config :sloanely_but_surely, Web.Endpoint,
url: [host: "localhost"],
adapter: Bandit.PhoenixAdapter,
render_errors: [
formats: [html: Web.ErrorHTML, json: Web.ErrorJSON],
layout: false
],
pubsub_server: Core.PubSub,
live_view: [signing_salt: "afQxdsCJ"]
config :sloanely_but_surely,
namespace: Core,
ecto_repos: [Core.Repo],
generators: [timestamp_type: :utc_datetime, binary_id: true]
config :tailwind,
version: "3.4.3",
cms: [
sloanely_but_surely: [
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__)
]

View file

@ -1,82 +1,39 @@
import Config
# 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 :logger, :console, format: "[$level] $message\n"
config :phoenix,
plug_init_mode: :runtime,
stacktrace_depth: 20
config :phoenix_live_view,
debug_heex_annotations: true,
enable_expensive_runtime_checks: true
config :sloanely_but_surely, Core.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "cms_dev",
database: "sloanely_but_surely_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
config :cms, CMSWeb.Endpoint,
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
config :sloanely_but_surely, Web.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4000],
check_origin: false,
code_reloader: true,
debug_errors: true,
secret_key_base: "OiPAYORPwZPHThILCLsbcM9fqJNWoTphDydEEXsrKaILQm1lz8lt0DMiu9cCoVqC",
watchers: [
esbuild: {Esbuild, :install_and_run, [:cms, ~w(--sourcemap=inline --watch)]},
tailwind: {Tailwind, :install_and_run, [:cms, ~w(--watch)]}
]
# Watch static and templates for browser reloading.
config :cms, CMSWeb.Endpoint,
esbuild: {Esbuild, :install_and_run, [:sloanely_but_surely, ~w(--sourcemap=inline --watch)]},
tailwind: {Tailwind, :install_and_run, [:sloanely_but_surely, ~w(--watch)]}
],
live_reload: [
patterns: [
~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
~r"lib/cms_web/(controllers|live|components)/.*(ex|heex)$"
~r"lib/web/(controllers|live|components)/.*(ex|heex)$"
]
]
# ## SSL Support
#
# 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
# 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"
# Initialize plugs at runtime for faster development compilation
# The `http:` config above can be replaced with:
#
# https: [
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,
# Include HEEx debug annotations as HTML comments in rendered markup
# ],
#
debug_heex_annotations: true,
# 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
config :sloanely_but_surely, dev_routes: true

View file

@ -1,14 +1,5 @@
import Config
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix assets.deploy` task,
# which you should run after static files are built and
# before starting your production server.
config :cms, CMSWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
# Do not print debug messages in production
config :logger, level: :info
# Runtime production configuration, including reading
# of environment variables, is done on config/runtime.exs.
config :sloanely_but_surely, Web.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"

View file

@ -1,26 +1,10 @@
import Config
# config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the
# system starts, so it is typically used to load production configuration
# and secrets from environment variables or elsewhere. Do not define
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.
# ## Using releases
#
# If you use `mix release`, you need to explicitly enable the server
# by passing the PHX_SERVER=true when you start it:
#
# PHX_SERVER=true bin/cms start
#
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above.
if System.get_env("PHX_SERVER") do
config :cms, CMSWeb.Endpoint, server: true
config :sloanely_but_surely, Web.Endpoint, server: true
end
config :cms,
config :sloanely_but_surely,
password_hash:
System.get_env("PASSWORD_HASH") ||
raise("""
@ -39,11 +23,6 @@ if config_env() == :prod do
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
# 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
# want to use a different value for prod and you most likely don't want
# to check this value into version control, so we use an environment
# variable instead.
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
@ -54,55 +33,17 @@ if config_env() == :prod do
host = System.get_env("PHX_HOST") || "example.com"
port = String.to_integer(System.get_env("PORT") || "4000")
config :cms, CMS.Repo,
config :sloanely_but_surely, Core.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 :sloanely_but_surely, Web.Endpoint,
url: [host: host, port: 443, scheme: "https"],
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
# See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: port
],
secret_key_base: secret_key_base
config :cms, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to your endpoint configuration:
#
# config :cms, CMSWeb.Endpoint,
# https: [
# ...,
# port: 443,
# cipher_suite: :strong,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
# ]
#
# The `cipher_suite` is set to `:strong` to support only the
# latest and more secure SSL ciphers. This means old browsers
# and clients may not be supported. You can set it to
# `:compatible` for wider support.
#
# `:keyfile` and `:certfile` expect an absolute path to the key
# and cert in disk or a relative path inside priv, for example
# "priv/ssl/server.key". For all supported SSL configuration
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
#
# We also recommend setting `force_ssl` in your config/prod.exs,
# ensuring no data is ever sent via http, always redirecting to https:
#
# config :cms, CMSWeb.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
end

View file

@ -1,31 +1,21 @@
import Config
# Configure your database
#
# The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information.
config :cms, CMS.Repo,
config :logger, level: :warning
config :phoenix, :plug_init_mode, :runtime
config :phoenix_live_view,
enable_expensive_runtime_checks: true
config :sloanely_but_surely, Core.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "cms_test#{System.get_env("MIX_TEST_PARTITION")}",
database: "sloanely_but_surely_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :cms, CMSWeb.Endpoint,
config :sloanely_but_surely, Web.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base: "IAv/74HChZvRYUunjUjCoj/b8NA6mZtVbcxv6ECoOJ+Xr+CeNBVGZ7zkDhUlXSq4",
server: false
# Print only warnings and errors during test
config :logger, level: :warning
# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime
# Enable helpful, but potentially expensive runtime checks
config :phoenix_live_view,
enable_expensive_runtime_checks: true

View file

@ -1,9 +0,0 @@
defmodule CMS do
@moduledoc """
CMS keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
end

View file

@ -1,34 +0,0 @@
defmodule CMS.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
CMSWeb.Telemetry,
CMS.Repo,
{DNSCluster, query: Application.get_env(:cms, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: CMS.PubSub},
# Start a worker by calling: CMS.Worker.start_link(arg)
# {CMS.Worker, arg},
# Start to serve requests, typically the last entry
CMSWeb.Endpoint
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: CMS.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
def config_change(changed, _new, removed) do
CMSWeb.Endpoint.config_change(changed, removed)
:ok
end
end

View file

@ -1,31 +0,0 @@
defmodule CMS.Posts do
@moduledoc false
import Ecto.Query
alias CMS.Posts.Post
alias CMS.Repo
def create_post(attrs) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
def update_post(post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
end
def get_post!(id) do
Repo.get!(Post, id)
end
def list_posts do
query =
from post in Post,
order_by: [desc: post.inserted_at]
Repo.all(query)
end
end

View file

@ -1,20 +0,0 @@
defmodule CMS.Posts.Post do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset, warn: false
@primary_key {:id, :binary_id, autogenerate: true}
schema "posts" do
field :title, :string
field :body, :string
timestamps()
end
def changeset(%__MODULE__{} = post, attrs \\ %{}) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:body])
end
end

View file

@ -1,5 +0,0 @@
defmodule CMS.Repo do
use Ecto.Repo,
otp_app: :cms,
adapter: Ecto.Adapters.Postgres
end

View file

@ -1,31 +0,0 @@
defmodule CMS.Statuses do
@moduledoc false
import Ecto.Query
alias CMS.Repo
alias CMS.Statuses.Status
def create_status(attrs) do
%Status{}
|> Status.changeset(attrs)
|> Repo.insert()
end
def update_status(status, attrs) do
status
|> Status.changeset(attrs)
|> Repo.update()
end
def get_status!(id) do
Repo.get!(Status, id)
end
def list_statuses do
query =
from status in Status,
order_by: [desc: status.inserted_at]
Repo.all(query)
end
end

View file

@ -1,19 +0,0 @@
defmodule CMS.Statuses.Status do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "statuses" do
field :body
timestamps()
end
def changeset(%__MODULE__{} = status, attrs \\ %{}) do
status
|> cast(attrs, [:body])
|> validate_required([:body])
end
end

View file

@ -1,16 +0,0 @@
defmodule CMSWeb.PageController do
use CMSWeb, :controller
alias CMS.Posts
alias CMS.Statuses
def home(conn, _params) do
posts = Enum.take(Posts.list_posts(), 5)
statuses = Enum.take(Statuses.list_statuses(), 10)
conn
|> assign(:posts, posts)
|> assign(:statuses, statuses)
|> render(:home)
end
end

View file

@ -1,93 +0,0 @@
defmodule CMSWeb.Telemetry do
@moduledoc false
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
# Telemetry poller will execute the given period measurements
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.start.system_time",
unit: {:native, :millisecond}
),
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.start.system_time",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.exception.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.socket_connected.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_joined.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_handled_in.duration",
tags: [:event],
unit: {:native, :millisecond}
),
# Database Metrics
summary("cms.repo.query.total_time",
unit: {:native, :millisecond},
description: "The sum of the other measurements"
),
summary("cms.repo.query.decode_time",
unit: {:native, :millisecond},
description: "The time spent decoding the data received from the database"
),
summary("cms.repo.query.query_time",
unit: {:native, :millisecond},
description: "The time spent executing the query"
),
summary("cms.repo.query.queue_time",
unit: {:native, :millisecond},
description: "The time spent waiting for a database connection"
),
summary("cms.repo.query.idle_time",
unit: {:native, :millisecond},
description: "The time the connection spent waiting before being checked out for the query"
),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {CMSWeb, :count_users, []}
]
end
end

4
lib/core.ex Normal file
View file

@ -0,0 +1,4 @@
defmodule Core do
@moduledoc false
use Boundary, deps: [Schema], exports: [Author, Posts, Statuses]
end

View file

@ -1,4 +1,4 @@
defmodule CMS.Author do
defmodule Core.Author do
@moduledoc """
Properties of the author, Sloane
@ -26,7 +26,7 @@ defmodule CMS.Author do
given_name: "Sloane",
additional_name: "Loretta",
family_name: "Perrault",
nickname: "sloanelybutsurely",
nickname: "sloanely_but_surely",
email: "sloane@fastmail.com",
url: "https://sloanelybutsurely.com"
}

37
lib/core/posts.ex Normal file
View file

@ -0,0 +1,37 @@
defmodule Core.Posts do
@moduledoc false
import Ecto.Changeset
import Ecto.Query
alias Core.Repo
def changeset(%Schema.Post{} = post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:body])
end
def create_post(attrs) do
%Schema.Post{}
|> changeset(attrs)
|> Repo.insert()
end
def update_post(post, attrs) do
post
|> changeset(attrs)
|> Repo.update()
end
def get_post!(id) do
Repo.get!(Schema.Post, id)
end
def list_posts do
query =
from post in Schema.Post,
order_by: [desc: post.inserted_at]
Repo.all(query)
end
end

5
lib/core/repo.ex Normal file
View file

@ -0,0 +1,5 @@
defmodule Core.Repo do
use Ecto.Repo,
otp_app: :sloanely_but_surely,
adapter: Ecto.Adapters.Postgres
end

37
lib/core/statuses.ex Normal file
View file

@ -0,0 +1,37 @@
defmodule Core.Statuses do
@moduledoc false
import Ecto.Changeset
import Ecto.Query
alias Core.Repo
def changeset(%Schema.Status{} = status, attrs \\ %{}) do
status
|> cast(attrs, [:body])
|> validate_required([:body])
end
def create_status(attrs) do
%Schema.Status{}
|> changeset(attrs)
|> Repo.insert()
end
def update_status(status, attrs) do
status
|> changeset(attrs)
|> Repo.update()
end
def get_status!(id) do
Repo.get!(Schema.Status, id)
end
def list_statuses do
query =
from status in Schema.Status,
order_by: [desc: status.inserted_at]
Repo.all(query)
end
end

View file

@ -1,8 +1,9 @@
defmodule Mix.Tasks.Cms.Gen.PasswordHash do
defmodule Mix.Tasks.SloanelyButSurely.Gen.PasswordHash do
@shortdoc @moduledoc
@moduledoc """
Hashes a password for the admin account
"""
use Boundary, classify_to: SloanelyButSurely.Mix
use Mix.Task
@impl Mix.Task

4
lib/schema.ex Normal file
View file

@ -0,0 +1,4 @@
defmodule Schema do
@moduledoc false
use Boundary, deps: [], exports: [Post, Status]
end

12
lib/schema/post.ex Normal file
View file

@ -0,0 +1,12 @@
defmodule Schema.Post do
@moduledoc false
use Ecto.Schema
@primary_key {:id, :binary_id, autogenerate: true}
schema "posts" do
field :title, :string
field :body, :string
timestamps()
end
end

11
lib/schema/status.ex Normal file
View file

@ -0,0 +1,11 @@
defmodule Schema.Status do
@moduledoc false
use Ecto.Schema
@primary_key {:id, :binary_id, autogenerate: true}
schema "statuses" do
field :body
timestamps()
end
end

View file

@ -0,0 +1,4 @@
defmodule SloanelyButSurely do
@moduledoc false
use Boundary, top_level?: true, deps: [Core, Web]
end

View file

@ -0,0 +1,21 @@
defmodule SloanelyButSurely.Application do
@moduledoc false
use Application
@impl Application
def start(_type, _args) do
children = [
Core.Repo,
{Phoenix.PubSub, name: Core.PubSub},
Web.Endpoint
]
Supervisor.start_link(children, strategy: :one_for_one, name: Core.Supervisor)
end
@impl Application
def config_change(changed, _new, removed) do
Web.Endpoint.config_change(changed, removed)
:ok
end
end

View file

@ -0,0 +1,4 @@
defmodule SloanelyButSurely.Mix do
@moduledoc false
use Boundary, deps: [], exports: []
end

View file

@ -1,21 +1,6 @@
defmodule CMSWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, components, channels, and so on.
This can be used in your application as:
use CMSWeb, :controller
use CMSWeb, :html
The definitions below will be executed for every controller,
component, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define additional modules and import
those modules here.
"""
defmodule Web do
@moduledoc false
use Boundary, deps: [Core, Schema], exports: [Endpoint]
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
@ -41,7 +26,7 @@ defmodule CMSWeb do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: CMSWeb.Layouts]
layouts: [html: Web.Layouts]
import Plug.Conn
@ -52,7 +37,7 @@ defmodule CMSWeb do
def live_view do
quote do
use Phoenix.LiveView,
layout: {CMSWeb.Layouts, :app}
layout: {Web.Layouts, :app}
unquote(html_helpers())
end
@ -81,9 +66,9 @@ defmodule CMSWeb do
defp html_helpers do
quote do
import CMSWeb.CoreComponents
# HTML escaping functionality
import Phoenix.HTML
import Web.CoreComponents
# HTML escaping functionality
# Core UI components
# Shortcut for generating JS commands
@ -97,9 +82,9 @@ defmodule CMSWeb do
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: CMSWeb.Endpoint,
router: CMSWeb.Router,
statics: CMSWeb.static_paths()
endpoint: Web.Endpoint,
router: Web.Router,
statics: Web.static_paths()
end
end

View file

@ -1,4 +1,4 @@
defmodule CMSWeb.CoreComponents do
defmodule Web.CoreComponents do
@moduledoc """
Provides core UI components.
"""

View file

@ -1,14 +1,13 @@
defmodule CMSWeb.Layouts do
defmodule Web.Layouts do
@moduledoc """
This module holds different layouts used by your application.
See the `layouts` directory for all templates available.
The "root" layout is a skeleton rendered as part of the
application router. The "app" layout is set as the default
layout on both `use CMSWeb, :controller` and
`use CMSWeb, :live_view`.
layout on both `use Web, :controller` and `use Web, :live_view`.
"""
use CMSWeb, :html
use Web, :html
embed_templates "layouts/*"

View file

@ -1,6 +1,6 @@
defmodule CMSWeb.AdminAuth do
defmodule Web.AdminAuth do
@moduledoc false
use CMSWeb, :verified_routes
use Web, :verified_routes
import Phoenix.Controller
import Plug.Conn
@ -14,7 +14,7 @@ defmodule CMSWeb.AdminAuth do
def log_out_admin(conn, params) do
if live_socket_id = get_session(conn, :live_socket_id) do
CMSWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
Web.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
@ -35,7 +35,7 @@ defmodule CMSWeb.AdminAuth do
end
def correct_password?(password) do
password_hash = Application.fetch_env!(:cms, :password_hash)
password_hash = Application.fetch_env!(:sloanely_but_surely, :password_hash)
Argon2.verify_pass(password, password_hash)
end

View file

@ -1,7 +1,7 @@
defmodule CMSWeb.AdminSessionController do
use CMSWeb, :controller
defmodule Web.AdminSessionController do
use Web, :controller
alias CMSWeb.AdminAuth
alias Web.AdminAuth
def create(conn, %{"password" => password} = params) do
if AdminAuth.correct_password?(password) do

View file

@ -1,10 +1,10 @@
defmodule CMSWeb.ErrorHTML do
defmodule Web.ErrorHTML do
@moduledoc """
This module is invoked by your endpoint in case of errors on HTML requests.
See config/config.exs.
"""
use CMSWeb, :html
use Web, :html
# If you want to customize your error pages,
# uncomment the embed_templates/1 call below

View file

@ -1,4 +1,4 @@
defmodule CMSWeb.ErrorJSON do
defmodule Web.ErrorJSON do
@moduledoc """
This module is invoked by your endpoint in case of errors on JSON requests.

View file

@ -1,6 +1,6 @@
defmodule CMSWeb.Globals do
defmodule Web.Globals do
@moduledoc false
use CMSWeb, :live_view
use Web, :live_view
def assign_globals(%Plug.Conn{} = conn, _opts) do
conn

View file

@ -0,0 +1,13 @@
defmodule Web.PageController do
use Web, :controller
def home(conn, _params) do
posts = Enum.take(Core.Posts.list_posts(), 5)
statuses = Enum.take(Core.Statuses.list_statuses(), 10)
conn
|> assign(:posts, posts)
|> assign(:statuses, statuses)
|> render(:home)
end
end

View file

@ -1,10 +1,10 @@
defmodule CMSWeb.PageHTML do
defmodule Web.PageHTML do
@moduledoc """
This module contains pages rendered by PageController.
See the `page_html` directory for all templates available.
"""
use CMSWeb, :html
use Web, :html
embed_templates "page_html/*"
end

View file

@ -1,10 +1,8 @@
defmodule CMSWeb.PostController do
use CMSWeb, :controller
alias CMS.Posts
defmodule Web.PostController do
use Web, :controller
def index(conn, _params) do
posts = Posts.list_posts()
posts = Core.Posts.list_posts()
conn
|> assign(:posts, posts)
@ -12,7 +10,7 @@ defmodule CMSWeb.PostController do
end
def show(conn, %{"post_id" => post_id}) do
post = Posts.get_post!(post_id)
post = Core.Posts.get_post!(post_id)
conn
|> assign(:post, post)

View file

@ -1,6 +1,6 @@
defmodule CMSWeb.PostHTML do
defmodule Web.PostHTML do
@moduledoc false
use CMSWeb, :html
use Web, :html
embed_templates "post_html/*"
end

View file

@ -1,10 +1,8 @@
defmodule CMSWeb.StatusController do
use CMSWeb, :controller
alias CMS.Statuses
defmodule Web.StatusController do
use Web, :controller
def index(conn, _params) do
statuses = Statuses.list_statuses()
statuses = Core.Statuses.list_statuses()
conn
|> assign(:statuses, statuses)
@ -12,7 +10,7 @@ defmodule CMSWeb.StatusController do
end
def show(conn, %{"status_id" => status_id}) do
status = Statuses.get_status!(status_id)
status = Core.Statuses.get_status!(status_id)
conn
|> assign(:status, status)

View file

@ -1,6 +1,6 @@
defmodule CMSWeb.StatusHTML do
defmodule Web.StatusHTML do
@moduledoc false
use CMSWeb, :html
use Web, :html
embed_templates "status_html/*"
end

View file

@ -1,5 +1,5 @@
defmodule CMSWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :cms
defmodule Web.Endpoint do
use Phoenix.Endpoint, otp_app: :sloanely_but_surely
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
@ -21,9 +21,9 @@ defmodule CMSWeb.Endpoint do
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :cms,
from: :sloanely_but_surely,
gzip: false,
only: CMSWeb.static_paths()
only: Web.static_paths()
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
@ -31,7 +31,7 @@ defmodule CMSWeb.Endpoint do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :cms
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :sloanely_but_surely
end
plug Phoenix.LiveDashboard.RequestLogger,
@ -49,5 +49,5 @@ defmodule CMSWeb.Endpoint do
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug CMSWeb.Router
plug Web.Router
end

View file

@ -1,6 +1,6 @@
defmodule CMSWeb.AdminLive do
defmodule Web.AdminLive do
@moduledoc false
use CMSWeb, :live_view
use Web, :live_view
@impl true
def mount(_params, _session, socket) do

View file

@ -1,6 +1,6 @@
defmodule CMSWeb.AdminLoginLive do
defmodule Web.AdminLoginLive do
@moduledoc false
use CMSWeb, :live_view
use Web, :live_view
@impl true
def mount(params, _session, socket) do

View file

@ -1,9 +1,6 @@
defmodule CMSWeb.PostLive do
defmodule Web.PostLive do
@moduledoc false
use CMSWeb, :live_view
alias CMS.Posts
alias CMS.Posts.Post
use Web, :live_view
@impl true
def mount(_params, _session, socket) do
@ -14,8 +11,8 @@ defmodule CMSWeb.PostLive do
@impl true
def handle_params(_params, _uri, %{assigns: %{live_action: :new}} = socket) do
post = %Post{}
changeset = Post.changeset(post)
post = %Schema.Post{}
changeset = Core.Posts.changeset(post, %{})
socket = assign(socket, post: post, form: to_form(changeset))
@ -23,9 +20,9 @@ defmodule CMSWeb.PostLive do
end
def handle_params(%{"post_id" => post_id}, _uri, %{assigns: %{live_action: :edit}} = socket) do
post = Posts.get_post!(post_id)
post = Core.Posts.get_post!(post_id)
changeset = Post.changeset(post)
changeset = Core.Posts.changeset(post, %{})
socket = assign(socket, post: post, form: to_form(changeset))
@ -35,7 +32,7 @@ defmodule CMSWeb.PostLive do
@impl true
def handle_event("save_post", %{"post" => attrs}, %{assigns: %{live_action: :new}} = socket) do
socket =
case Posts.create_post(attrs) do
case Core.Posts.create_post(attrs) do
{:ok, post} -> push_navigate(socket, to: ~p"/admin/posts/#{post}")
{:error, changeset} -> assign(socket, form: to_form(changeset))
end
@ -45,9 +42,15 @@ defmodule CMSWeb.PostLive do
def handle_event("save_post", %{"post" => attrs}, %{assigns: %{post: post, live_action: :edit}} = socket) do
socket =
case Posts.update_post(post, attrs) do
case Core.Posts.update_post(post, attrs) do
{:ok, post} ->
assign(socket, post: post, form: post |> Post.changeset() |> to_form())
assign(socket,
post: post,
form:
post
|> Core.Posts.changeset(%{})
|> to_form()
)
{:error, changeset} ->
assign(socket, form: to_form(changeset))

View file

@ -1,9 +1,6 @@
defmodule CMSWeb.StatusLive do
defmodule Web.StatusLive do
@moduledoc false
use CMSWeb, :live_view
alias CMS.Statuses
alias CMS.Statuses.Status
use Web, :live_view
@impl true
def mount(_params, _session, socket) do
@ -12,9 +9,9 @@ defmodule CMSWeb.StatusLive do
@impl true
def handle_params(_params, _uri, %{assigns: %{live_action: :new}} = socket) do
status = %Status{}
status = %Schema.Status{}
changeset = Status.changeset(status)
changeset = Core.Statuses.changeset(status, %{})
socket = assign(socket, status: status, form: to_form(changeset))
@ -22,9 +19,9 @@ defmodule CMSWeb.StatusLive do
end
def handle_params(%{"status_id" => status_id}, _uri, %{assigns: %{live_action: :edit}} = socket) do
status = Statuses.get_status!(status_id)
status = Core.Statuses.get_status!(status_id)
changeset = Status.changeset(status)
changeset = Core.Statuses.changeset(status, %{})
socket = assign(socket, status: status, form: to_form(changeset))
@ -34,7 +31,7 @@ defmodule CMSWeb.StatusLive do
@impl true
def handle_event("save_status", %{"status" => attrs}, %{assigns: %{live_action: :new}} = socket) do
socket =
case Statuses.create_status(attrs) do
case Core.Statuses.create_status(attrs) do
{:ok, status} -> push_navigate(socket, to: ~p"/admin/statuses/#{status}")
{:error, changeset} -> assign(socket, form: to_form(changeset))
end
@ -42,15 +39,17 @@ defmodule CMSWeb.StatusLive do
{:noreply, socket}
end
def handle_event(
"save_status",
%{"status" => attrs},
%{assigns: %{status: status, live_action: :edit}} = socket
) do
def handle_event("save_status", %{"status" => attrs}, %{assigns: %{status: status, live_action: :edit}} = socket) do
socket =
case Statuses.update_status(status, attrs) do
case Core.Statuses.update_status(status, attrs) do
{:ok, status} ->
assign(socket, status: status, form: to_form(Status.changeset(status)))
assign(socket,
status: status,
form:
status
|> Core.Statuses.changeset(%{})
|> to_form()
)
{:error, changeset} ->
assign(socket, form: to_form(changeset))

View file

@ -1,17 +1,17 @@
defmodule CMSWeb.Router do
use CMSWeb, :router
defmodule Web.Router do
use Web, :router
import CMSWeb.AdminAuth
import CMSWeb.Globals
import Web.AdminAuth
import Web.Globals
alias CMSWeb.AdminAuth
alias CMSWeb.Globals
alias Web.AdminAuth
alias Web.Globals
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {CMSWeb.Layouts, :root}
plug :put_root_layout, html: {Web.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :assign_globals
@ -27,7 +27,7 @@ defmodule CMSWeb.Router do
end
live_session :default, on_mount: [AdminAuth, Globals] do
scope "/", CMSWeb do
scope "/", Web do
pipe_through :browser
pipe_through :supports_admin_action
@ -44,7 +44,7 @@ defmodule CMSWeb.Router do
get "/admin/session/destroy", AdminSessionController, :destroy
end
scope "/admin", CMSWeb do
scope "/admin", Web do
pipe_through :browser
pipe_through :requires_admin
@ -57,20 +57,4 @@ defmodule CMSWeb.Router do
live "/statuses/:status_id", StatusLive, :edit
end
end
# Enable LiveDashboard in development
if Application.compile_env(:cms, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
import Phoenix.LiveDashboard.Router
scope "/dev" do
pipe_through :browser
live_dashboard "/dashboard", metrics: CMSWeb.Telemetry
end
end
end

13
mix.exs
View file

@ -1,12 +1,13 @@
defmodule CMS.MixProject do
defmodule SlaonelyButSurely.MixProject do
use Mix.Project
def project do
[
app: :cms,
version: "0.1.0",
app: :sloanely_but_surely,
version: "1.0.0",
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:boundary] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
@ -18,7 +19,7 @@ defmodule CMS.MixProject do
# Type `mix help compile.app` for more information.
def application do
[
mod: {CMS.Application, []},
mod: {SloanelyButSurely.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
@ -48,14 +49,14 @@ defmodule CMS.MixProject do
{:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.5"},
{:argon2_elixir, "~> 4.1"},
{:timex, "~> 3.7"},
{:typed_struct, "~> 0.3.0"},
# dev/test only
{:styler, "~> 1.4", only: [:dev, :test], runtime: false}
{:styler, "~> 1.4", only: [:dev, :test], runtime: false},
{:boundary, "~> 0.10.4"}
]
end

View file

@ -1,6 +1,7 @@
%{
"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"},
"boundary": {:hex, :boundary, "0.10.4", "5fec5d2736c12f9bfe1720c3a2bd8c48c3547c24d6002ebf8e087570afd5bd2f", [:mix], [], "hexpm", "8baf6f23987afdb1483033ed0bde75c9c703613c22ed58d5f23bf948f203247c"},
"castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"},
"certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},

View file

@ -1,4 +1,4 @@
defmodule CMS.Repo.Migrations.AddPostsTable do
defmodule Core.Repo.Migrations.AddPostsTable do
use Ecto.Migration
def change do

View file

@ -1,4 +1,4 @@
defmodule CMS.Repo.Migrations.AddStatusesTable do
defmodule Core.Repo.Migrations.AddStatusesTable do
use Ecto.Migration
def change do

View file

@ -5,7 +5,7 @@
# Inside the script, you can read and write to any of your
# repositories directly:
#
# CMS.Repo.insert!(%CMS.SomeSchema{})
# Core.Repo.insert!(%CMS.SomeSchema{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.

View file

@ -1,14 +0,0 @@
defmodule CMSWeb.ErrorHTMLTest do
use CMSWeb.ConnCase, async: true
# Bring render_to_string/4 for testing custom views
import Phoenix.Template
test "renders 404.html" do
assert render_to_string(CMSWeb.ErrorHTML, "404", "html", []) == "Not Found"
end
test "renders 500.html" do
assert render_to_string(CMSWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
end
end

View file

@ -1,12 +0,0 @@
defmodule CMSWeb.ErrorJSONTest do
use CMSWeb.ConnCase, async: true
test "renders 404" do
assert CMSWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
end
test "renders 500" do
assert CMSWeb.ErrorJSON.render("500.json", %{}) ==
%{errors: %{detail: "Internal Server Error"}}
end
end

View file

@ -1,8 +0,0 @@
defmodule CMSWeb.PageControllerTest do
use CMSWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
end
end

View file

@ -1,4 +1,4 @@
defmodule CMSWeb.ConnCase do
defmodule Test.ConnCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a connection.
@ -11,7 +11,7 @@ defmodule CMSWeb.ConnCase do
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use CMSWeb.ConnCase, async: true`, although
by setting `use Test.ConnCase, async: true`, although
this option is not recommended for other databases.
"""
@ -19,20 +19,21 @@ defmodule CMSWeb.ConnCase do
using do
quote do
use CMSWeb, :verified_routes
use Web, :verified_routes
import CMSWeb.ConnCase
import Phoenix.ConnTest
import Plug.Conn
import Test.ConnCase
# The default endpoint for testing
@endpoint CMSWeb.Endpoint
@endpoint Web.Endpoint
# Import conveniences for testing with connections
end
end
setup tags do
CMS.DataCase.setup_sandbox(tags)
Test.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end

View file

@ -1,4 +1,4 @@
defmodule CMS.DataCase do
defmodule Test.DataCase do
@moduledoc """
This module defines the setup for tests requiring
access to the application's data layer.
@ -10,7 +10,7 @@ defmodule CMS.DataCase do
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use CMS.DataCase, async: true`, although
by setting `use Test.DataCase, async: true`, although
this option is not recommended for other databases.
"""
@ -20,17 +20,17 @@ defmodule CMS.DataCase do
using do
quote do
import CMS.DataCase
import Ecto
import Ecto.Changeset
import Ecto.Query
import Test.DataCase
alias CMS.Repo
alias Core.Repo
end
end
setup tags do
CMS.DataCase.setup_sandbox(tags)
Test.DataCase.setup_sandbox(tags)
:ok
end
@ -38,7 +38,7 @@ defmodule CMS.DataCase do
Sets up the sandbox based on the test tags.
"""
def setup_sandbox(tags) do
pid = Sandbox.start_owner!(CMS.Repo, shared: not tags[:async])
pid = Sandbox.start_owner!(Core.Repo, shared: not tags[:async])
on_exit(fn -> Sandbox.stop_owner(pid) end)
end

3
test/support/test.ex Normal file
View file

@ -0,0 +1,3 @@
defmodule Test do
use Boundary, top_level?: true, check: [in: false, out: false]
end

View file

@ -1,2 +1,2 @@
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(CMS.Repo, :manual)
Ecto.Adapters.SQL.Sandbox.mode(Core.Repo, :manual)

View file

@ -0,0 +1,14 @@
defmodule Test.Web.ErrorHTMLTest do
use Test.ConnCase, async: true
# Bring render_to_string/4 for testing custom views
import Phoenix.Template
test "renders 404.html" do
assert render_to_string(Web.ErrorHTML, "404", "html", []) == "Not Found"
end
test "renders 500.html" do
assert render_to_string(Web.ErrorHTML, "500", "html", []) == "Internal Server Error"
end
end

View file

@ -0,0 +1,12 @@
defmodule Test.Web.ErrorJSONTest do
use Test.ConnCase, async: true
test "renders 404" do
assert Web.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
end
test "renders 500" do
assert Web.ErrorJSON.render("500.json", %{}) ==
%{errors: %{detail: "Internal Server Error"}}
end
end

View file

@ -0,0 +1,8 @@
defmodule Test.Web.PageControllerTest do
use Test.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "sloanelybutsurely.com"
end
end