diff --git a/.formatter.exs b/.formatter.exs
index 8a6391c..ef8840c 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -1,5 +1,6 @@
[
- import_deps: [:ecto, :phoenix],
- inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
- subdirectories: ["priv/*/migrations"]
+ import_deps: [:ecto, :ecto_sql, :phoenix],
+ subdirectories: ["priv/*/migrations"],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
]
diff --git a/LICENSE.md b/LICENSE.md
index 52d4056..49a3dd2 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright © 2021 - 2022 Conrad Taylor. All Rights Reserved.
+Copyright © 2021 - 2024 Conrad Taylor. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index cf23c28..75f6927 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ The purpose of this project is to implement an application where fans can commen
- Erlang 26.2.3 or newer
-- Phoenix 1.6.16 or newer
+- Phoenix 1.17.11 or newer
- PostgreSQL 15.6 or newer
diff --git a/config/config.exs b/config/config.exs
index f8f1855..e3e4d05 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -9,15 +9,18 @@ import Config
config :flix,
ecto_repos: [Flix.Repo],
- generators: [binary_id: true]
+ generators: [timestamp_type: :utc_datetime, binary_id: true]
# Configures the endpoint
config :flix, FlixWeb.Endpoint,
- adapter: Bandit.PhoenixAdapter,
url: [host: "localhost"],
- render_errors: [view: FlixWeb.ErrorView, accepts: ~w(html json), layout: false],
+ adapter: Bandit.PhoenixAdapter,
+ render_errors: [
+ formats: [html: FlixWeb.ErrorHTML, json: FlixWeb.ErrorJSON],
+ layout: false
+ ],
pubsub_server: Flix.PubSub,
- live_view: [signing_salt: "C/wWe4Zy"]
+ live_view: [signing_salt: "6jqhXaaD"]
# Configures the mailer
#
@@ -33,33 +36,24 @@ config :swoosh, :api_client, false
# Configure esbuild (the version is required)
config :esbuild,
- version: "0.18.11",
- default: [
+ version: "0.17.11",
+ flix: [
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__)}
]
-# Configure dart_sass (the version is required)
-config :dart_sass,
- version: "1.61.0",
- default: [
- args: ~w(css/app.scss ../priv/static/assets/app.css.tailwind),
- cd: Path.expand("../assets", __DIR__)
- ]
-
# Configure tailwind (the version is required)
config :tailwind,
- version: "3.3.2",
- default: [
+ version: "3.4.3",
+ flix: [
args: ~w(
--config=tailwind.config.js
- --input=../priv/static/assets/app.css.tailwind
+ --input=css/app.css
--output=../priv/static/assets/app.css
),
- cd: Path.expand("../assets", __DIR__),
- env: %{"BROWSERSLIST_IGNORE_OLD_DATA" => "1"}
+ cd: Path.expand("../assets", __DIR__)
]
# Configures Elixir's Logger
@@ -70,16 +64,6 @@ config :logger, :console,
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
-# Configure the time zone database.
-# https://mikezornek.com/posts/2020/3/working-with-time-zones-in-an-elixir-phoenix-app
-# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
-config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
-
-# Configure Bamboo Mailer
-config :flix, Flix.Mailer,
- adapter: Bamboo.SendGridAdapter,
- api_key: System.get_env("SENDGRID_API_KEY")
-
# Configure mix_test_watch
if Mix.env() == :dev do
config :mix_test_watch,
diff --git a/config/dev.exs b/config/dev.exs
index 28b6619..a55fee9 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -4,8 +4,9 @@ import Config
config :flix, Flix.Repo,
username: "postgres",
password: "postgres",
- database: "flix_dev",
hostname: "localhost",
+ database: "flix_dev",
+ stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
@@ -13,8 +14,8 @@ config :flix, Flix.Repo,
# debugging and code reloading.
#
# The watchers configuration can be used to run external
-# watchers to your application. For example, we use it
-# with esbuild to recompile .js and .css sources.
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
config :flix, FlixWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
@@ -22,21 +23,10 @@ config :flix, FlixWeb.Endpoint,
check_origin: false,
code_reloader: true,
debug_errors: true,
- secret_key_base: "tCI4Bo4USd+jHzx1Wj3qeOT4/rhp60jxq/QJqsod9pf4aI/lHaaNAMkgX/9dwM4d",
+ secret_key_base: "cAV3Rs7tYjMMYLC4iKYTar/gSG0sBrfmUXPHKGOr+I+MRlcHTGxmR8N8pTbMkRM3",
watchers: [
- # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
- esbuild: {
- Esbuild,
- :install_and_run,
- [:default, ~w(--sourcemap=inline --watch)]},
- # Start the sass watcher by calling DartSass.install_and_run(:default, args)
- sass: {
- DartSass,
- :install_and_run,
- [:default, ~w(--embed-source-map --source-map-urls=absolute --watch)]
- },
- # Start the tailwind watcher by calling Tailwind.install_and_run(:default, args)
- tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
+ esbuild: {Esbuild, :install_and_run, [:flix, ~w(--sourcemap=inline --watch)]},
+ tailwind: {Tailwind, :install_and_run, [:flix, ~w(--watch)]}
]
# ## SSL Support
@@ -47,7 +37,6 @@ config :flix, FlixWeb.Endpoint,
#
# mix phx.gen.cert
#
-# Note that this task requires Erlang/OTP 20 or later.
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
@@ -67,13 +56,15 @@ config :flix, FlixWeb.Endpoint,
config :flix, FlixWeb.Endpoint,
live_reload: [
patterns: [
- ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
- ~r"lib/flix_web/(live|views)/.*(ex)$",
- ~r"lib/flix_web/templates/.*(eex)$"
+ ~r"lib/flix_web/(controllers|live|components)/.*(ex|heex)$"
]
]
+# Enable dev routes for dashboard and mailbox
+config :flix, dev_routes: true
+
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
@@ -84,12 +75,13 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
+# Include HEEx debug annotations as HTML comments in rendered markup
+config :phoenix_live_view, :debug_heex_annotations, true
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
+
# Set configuration for image upload.
config :waffle,
storage: Waffle.Storage.Local,
asset_host: "http://localhost:4000"
-
-# Set configuation for sending e-mail.
-config :flix, Flix.Mailer,
- adapter: Bamboo.LocalAdapter,
- open_email_in_browser_url: "http://localhost:4000/sent_emails"
diff --git a/config/prod.exs b/config/prod.exs
index 1946c6b..ecaa16f 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -1,23 +1,24 @@
import Config
-# For production, don't forget to configure the url host
-# to something meaningful, Phoenix uses this information
-# when generating URLs.
-#
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
-# manifest is generated by the `mix phx.digest` task,
+# 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 :flix, FlixWeb.Endpoint,
- http: [port: {:system, "PORT"}],
- url: [scheme: "https", host: "flix-elixir-cwt.herokuapp.com", port: 443],
- force_ssl: [rewrite_on: [:x_forwarded_proto]],
- cache_static_manifest: "priv/static/cache_manifest.json"
+config :flix, FlixWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Configures Swoosh API Client
+config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Flix.Finch
+
+# Disable Swoosh Local Memory Storage
+config :swoosh, local: false
# 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 :waffle,
storage: Waffle.Storage.S3,
bucket: System.get_env("AWS_BUCKET_NAME"),
diff --git a/config/runtime.exs b/config/runtime.exs
index 845b3cc..739fb90 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -6,6 +6,20 @@ import Config
# 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/flix 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 :flix, FlixWeb.Endpoint, server: true
+end
+
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
@@ -14,11 +28,13 @@ if config_env() == :prod do
For example: ecto://USER:PASS@HOST/DATABASE
"""
+ maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
+
config :flix, Flix.Repo,
# ssl: true,
- # socket_options: [:inet6],
url: database_url,
- pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
+ 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.
# A default value is used in config/dev.exs and config/test.exs but you
@@ -32,26 +48,54 @@ if config_env() == :prod do
You can generate one by calling: mix phx.gen.secret
"""
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :flix, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
+
config :flix, FlixWeb.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/plug_cowboy/Plug.Cowboy.html
+ # 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: String.to_integer(System.get_env("PORT") || "4000")
+ port: port
],
secret_key_base: secret_key_base
- # ## Using releases
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :flix, FlixWeb.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
#
- # If you are doing OTP releases, you need to instruct Phoenix
- # to start each relevant endpoint:
+ # We also recommend setting `force_ssl` in your config/prod.exs,
+ # ensuring no data is ever sent via http, always redirecting to https:
#
- # config :flix, FlixWeb.Endpoint, server: true
+ # config :flix, FlixWeb.Endpoint,
+ # force_ssl: [hsts: true]
#
- # Then you can assemble a release by calling `mix release`.
- # See `mix help release` for more information.
+ # Check `Plug.SSL` for all available options in `force_ssl`.
# ## Configuring the mailer
#
diff --git a/config/test.exs b/config/test.exs
index 41a1825..19f8d31 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -8,23 +8,26 @@ import Config
config :flix, Flix.Repo,
username: "postgres",
password: "postgres",
- database: "flix_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: "localhost",
+ database: "flix_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
- pool_size: 10
+ 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 :flix, FlixWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002],
- secret_key_base: "36x+Owok727HAPW0YsIMAqf6uF9/Rqpgzvg06aS+7+CGGs9QlkrQlPN5bB7QhlTv",
+ secret_key_base: "0tGKECPKVrlL6aWOZtMHm2MJZUwa7vhTz/EaK6YD2mgh2+HMNT+lpP6j1V7lc9+t",
server: false
# In test we don't send emails.
config :flix, Flix.Mailer, adapter: Swoosh.Adapters.Test
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
+
# Print only warnings and errors during test
-config :logger, level: :warn
+config :logger, level: :warning
# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime
diff --git a/lib/flix/accounts/user_notifier.ex b/lib/flix/accounts/user_notifier.ex
index 752943b..6c79b81 100644
--- a/lib/flix/accounts/user_notifier.ex
+++ b/lib/flix/accounts/user_notifier.ex
@@ -1,24 +1,79 @@
defmodule Flix.Accounts.UserNotifier do
- alias Flix.Email
+ import Swoosh.Email
+
+ alias Flix.Mailer
+
+ # Delivers the email using the application mailer.
+ defp deliver(recipient, subject, body) do
+ email =
+ new()
+ |> to(recipient)
+ |> from({"Flix", "contact@example.com"})
+ |> subject(subject)
+ |> text_body(body)
+
+ with {:ok, _metadata} <- Mailer.deliver(email) do
+ {:ok, email}
+ end
+ end
@doc """
Deliver instructions to confirm account.
"""
def deliver_confirmation_instructions(user, url) do
- Email.deliver_confirmation_instructions(user, url)
+ deliver(user.email, "Confirmation instructions", """
+
+ ==============================
+
+ Hi #{user.email},
+
+ You can confirm your account by visiting the URL below:
+
+ #{url}
+
+ If you didn't create an account with us, please ignore this.
+
+ ==============================
+ """)
end
@doc """
Deliver instructions to reset a user password.
"""
def deliver_reset_password_instructions(user, url) do
- Email.deliver_reset_password_instructions(user, url)
+ deliver(user.email, "Reset password instructions", """
+
+ ==============================
+
+ Hi #{user.email},
+
+ You can reset your password by visiting the URL below:
+
+ #{url}
+
+ If you didn't request this change, please ignore this.
+
+ ==============================
+ """)
end
@doc """
Deliver instructions to update a user email.
"""
def deliver_update_email_instructions(user, url) do
- Email.deliver_update_email_instructions(user, url)
+ deliver(user.email, "Update email instructions", """
+
+ ==============================
+
+ Hi #{user.email},
+
+ You can change your email by visiting the URL below:
+
+ #{url}
+
+ If you didn't request this change, please ignore this.
+
+ ==============================
+ """)
end
end
diff --git a/lib/flix/accounts/user_token.ex b/lib/flix/accounts/user_token.ex
index c66b3b0..f858b67 100644
--- a/lib/flix/accounts/user_token.ex
+++ b/lib/flix/accounts/user_token.ex
@@ -1,6 +1,7 @@
defmodule Flix.Accounts.UserToken do
use Ecto.Schema
import Ecto.Query
+ alias Flix.Accounts.UserToken
@hash_algorithm :sha256
@rand_size 32
@@ -27,20 +28,37 @@ defmodule Flix.Accounts.UserToken do
Generates a token that will be stored in a signed place,
such as session or cookie. As they are signed, those
tokens do not need to be hashed.
+
+ The reason why we store session tokens in the database, even
+ though Phoenix already provides a session cookie, is because
+ Phoenix' default session cookies are not persisted, they are
+ simply signed and potentially encrypted. This means they are
+ valid indefinitely, unless you change the signing/encryption
+ salt.
+
+ Therefore, storing them allows individual user
+ sessions to be expired. The token system can also be extended
+ to store additional data, such as the device used for logging in.
+ You could then use this information to display all valid sessions
+ and devices in the UI and allow users to explicitly expire any
+ session they deem invalid.
"""
def build_session_token(user) do
token = :crypto.strong_rand_bytes(@rand_size)
- {token, %Flix.Accounts.UserToken{token: token, context: "session", user_id: user.id}}
+ {token, %UserToken{token: token, context: "session", user_id: user.id}}
end
@doc """
Checks if the token is valid and returns its underlying lookup query.
- The query returns the user found by the token.
+ The query returns the user found by the token, if any.
+
+ The token is valid if it matches the value in the database and it has
+ not expired (after @session_validity_in_days).
"""
def verify_session_token_query(token) do
query =
- from token in token_and_context_query(token, "session"),
+ from token in by_token_and_context_query(token, "session"),
join: user in assoc(token, :user),
where: token.inserted_at > ago(@session_validity_in_days, "day"),
select: user
@@ -49,12 +67,17 @@ defmodule Flix.Accounts.UserToken do
end
@doc """
- Builds a token with a hashed counter part.
+ Builds a token and its hash to be delivered to the user's email.
The non-hashed token is sent to the user email while the
- hashed part is stored in the database, to avoid reconstruction.
- The token is valid for a week as long as users don't change
- their email.
+ hashed part is stored in the database. The original token cannot be reconstructed,
+ which means anyone with read-only access to the database cannot directly use
+ the token in the application to gain access. Furthermore, if the user changes
+ their email in the system, the tokens sent to the previous email are no longer
+ valid.
+
+ Users can easily adapt the existing code to provide other types of delivery methods,
+ for example, by phone numbers.
"""
def build_email_token(user, context) do
build_hashed_token(user, context, user.email)
@@ -65,7 +88,7 @@ defmodule Flix.Accounts.UserToken do
hashed_token = :crypto.hash(@hash_algorithm, token)
{Base.url_encode64(token, padding: false),
- %Flix.Accounts.UserToken{
+ %UserToken{
token: hashed_token,
context: context,
sent_to: sent_to,
@@ -76,7 +99,15 @@ defmodule Flix.Accounts.UserToken do
@doc """
Checks if the token is valid and returns its underlying lookup query.
- The query returns the user found by the token.
+ The query returns the user found by the token, if any.
+
+ The given token is valid if it matches its hashed counterpart in the
+ database and the user email has not changed. This function also checks
+ if the token is being used within a certain period, depending on the
+ context. The default contexts supported by this function are either
+ "confirm", for account confirmation emails, and "reset_password",
+ for resetting the password. For verifying requests to change the email,
+ see `verify_change_email_token_query/2`.
"""
def verify_email_token_query(token, context) do
case Base.url_decode64(token, padding: false) do
@@ -85,7 +116,7 @@ defmodule Flix.Accounts.UserToken do
days = days_for_context(context)
query =
- from token in token_and_context_query(hashed_token, context),
+ from token in by_token_and_context_query(hashed_token, context),
join: user in assoc(token, :user),
where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email,
select: user
@@ -103,15 +134,24 @@ defmodule Flix.Accounts.UserToken do
@doc """
Checks if the token is valid and returns its underlying lookup query.
- The query returns the user token record.
+ The query returns the user found by the token, if any.
+
+ This is used to validate requests to change the user
+ email. It is different from `verify_email_token_query/2` precisely because
+ `verify_email_token_query/2` validates the email has not changed, which is
+ the starting point by this function.
+
+ The given token is valid if it matches its hashed counterpart in the
+ database and if it has not expired (after @change_email_validity_in_days).
+ The context must always start with "change:".
"""
- def verify_change_email_token_query(token, context) do
+ def verify_change_email_token_query(token, "change:" <> _ = context) do
case Base.url_decode64(token, padding: false) do
{:ok, decoded_token} ->
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
query =
- from token in token_and_context_query(hashed_token, context),
+ from token in by_token_and_context_query(hashed_token, context),
where: token.inserted_at > ago(@change_email_validity_in_days, "day")
{:ok, query}
@@ -122,20 +162,20 @@ defmodule Flix.Accounts.UserToken do
end
@doc """
- Returns the given token with the given context.
+ Returns the token struct for the given token value and context.
"""
- def token_and_context_query(token, context) do
- from Flix.Accounts.UserToken, where: [token: ^token, context: ^context]
+ def by_token_and_context_query(token, context) do
+ from UserToken, where: [token: ^token, context: ^context]
end
@doc """
Gets all tokens for the given user for the given contexts.
"""
- def user_and_contexts_query(user, :all) do
- from t in Flix.Accounts.UserToken, where: t.user_id == ^user.id
+ def by_user_and_contexts_query(user, :all) do
+ from t in UserToken, where: t.user_id == ^user.id
end
- def user_and_contexts_query(user, [_ | _] = contexts) do
- from t in Flix.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts
+ def by_user_and_contexts_query(user, [_ | _] = contexts) do
+ from t in UserToken, where: t.user_id == ^user.id and t.context in ^contexts
end
end
diff --git a/lib/flix/application.ex b/lib/flix/application.ex
index c87c15a..9379fac 100644
--- a/lib/flix/application.ex
+++ b/lib/flix/application.ex
@@ -5,18 +5,19 @@ defmodule Flix.Application do
use Application
+ @impl true
def start(_type, _args) do
children = [
- # Start the Ecto repository
- Flix.Repo,
- # Start the Telemetry supervisor
FlixWeb.Telemetry,
- # Start the PubSub system
+ Flix.Repo,
+ {DNSCluster, query: Application.get_env(:flix, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: Flix.PubSub},
- # Start the Endpoint (http/https)
- FlixWeb.Endpoint
+ # Start the Finch HTTP client for sending emails
+ {Finch, name: Flix.Finch},
# Start a worker by calling: Flix.Worker.start_link(arg)
- # {Flix.Worker, arg}
+ # {Flix.Worker, arg},
+ # Start to serve requests, typically the last entry
+ FlixWeb.Endpoint
]
# See https://hexdocs.pm/elixir/Supervisor.html
@@ -27,6 +28,7 @@ defmodule Flix.Application do
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
+ @impl true
def config_change(changed, _new, removed) do
FlixWeb.Endpoint.config_change(changed, removed)
:ok
diff --git a/lib/flix/mailer.ex b/lib/flix/mailer.ex
index c0c0539..e15d475 100644
--- a/lib/flix/mailer.ex
+++ b/lib/flix/mailer.ex
@@ -1,3 +1,3 @@
defmodule Flix.Mailer do
- use Bamboo.Mailer, otp_app: :flix
+ use Swoosh.Mailer, otp_app: :flix
end
diff --git a/lib/flix_web.ex b/lib/flix_web.ex
index 7630918..8541967 100644
--- a/lib/flix_web.ex
+++ b/lib/flix_web.ex
@@ -1,61 +1,57 @@
defmodule FlixWeb do
@moduledoc """
The entrypoint for defining your web interface, such
- as controllers, views, channels and so on.
+ as controllers, components, channels, and so on.
This can be used in your application as:
use FlixWeb, :controller
- use FlixWeb, :view
+ use FlixWeb, :html
- The definitions below will be executed for every view,
- controller, etc, so keep them short and clean, focused
+ 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 any helper function in modules
- and import those modules here.
+ below. Instead, define additional modules and import
+ those modules here.
"""
- def controller do
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
quote do
- use Phoenix.Controller, namespace: FlixWeb
+ use Phoenix.Router, helpers: false
+ # Import common connection and controller functions to use in pipelines
import Plug.Conn
- import FlixWeb.Gettext
- alias FlixWeb.Router.Helpers, as: Routes
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
end
end
- def view do
+ def channel do
quote do
- use Phoenix.View,
- root: "lib/flix_web/templates",
- namespace: FlixWeb
-
- # Import convenience functions from controllers
- import Phoenix.Controller,
- only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
-
- import FlixWeb.Router.Helpers
+ use Phoenix.Channel
+ end
+ end
- # Include shared imports and aliases for views
- unquote(view_helpers())
+ def controller do
+ quote do
+ use Phoenix.Controller, namespace: FlixWeb
- # Include custom helpers.
- import FlixWeb.Helpers.TextHelper
- import Number.Currency
- import Phoenix.HTML.SimplifiedHelpers.Truncate
- import PhoenixMTM.Helpers
+ import Plug.Conn
+ import FlixWeb.Gettext
+ alias FlixWeb.Router.Helpers, as: Routes
end
end
def live_view do
quote do
use Phoenix.LiveView,
- layout: {FlixWeb.LayoutView, "live.html"}
+ layout: {FlixWeb.Layouts, :app}
- unquote(view_helpers())
+ unquote(html_helpers())
end
end
@@ -63,42 +59,51 @@ defmodule FlixWeb do
quote do
use Phoenix.LiveComponent
- unquote(view_helpers())
+ unquote(html_helpers())
end
end
- def router do
+ def html do
quote do
- use Phoenix.Router
+ use Phoenix.Component
- import Plug.Conn
- import Phoenix.Controller
- import Phoenix.LiveView.Router
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include custom helpers.
+ import FlixWeb.Helpers.TextHelper
+ import Number.Currency
+ import Phoenix.HTML.SimplifiedHelpers.Truncate
+ import PhoenixMTM.Helpers
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
end
end
- def channel do
+ defp html_helpers do
quote do
- use Phoenix.Channel
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components and translation
+ import FlixWeb.CoreComponents
import FlixWeb.Gettext
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
end
end
- defp view_helpers do
+ def verified_routes do
quote do
- # Use all HTML functionality (forms, tags, etc)
- use Phoenix.HTML
- use Phoenix.HTML.SimplifiedHelpers
-
- # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
- import Phoenix.LiveView.Helpers
-
- # Import basic rendering functionality (render, render_layout, etc)
- import Phoenix.View
-
- import FlixWeb.ErrorHelpers
- import FlixWeb.Gettext
- alias FlixWeb.Router.Helpers, as: Routes
+ use Phoenix.VerifiedRoutes,
+ endpoint: FlixWeb.Endpoint,
+ router: FlixWeb.Router,
+ statics: FlixWeb.static_paths()
end
end
diff --git a/lib/flix_web/components/core_components.ex b/lib/flix_web/components/core_components.ex
new file mode 100644
index 0000000..56394c4
--- /dev/null
+++ b/lib/flix_web/components/core_components.ex
@@ -0,0 +1,675 @@
+defmodule FlixWeb.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ At first glance, this module may seem daunting, but its goal is to provide
+ core building blocks for your application, such as modals, tables, and
+ forms. The components consist mostly of markup and are well-documented
+ with doc strings and declarative assigns. You may customize and style
+ them in any way you want, based on your application growth and needs.
+
+ The default components use Tailwind CSS, a utility-first CSS framework.
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
+ how to customize them or feel free to swap in another framework altogether.
+
+ Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+ import FlixWeb.Gettext
+
+ @doc """
+ Renders a modal.
+
+ ## Examples
+
+ <.modal id="confirm-modal">
+ This is a modal.
+
+
+ JS commands may be passed to the `:on_cancel` to configure
+ the closing/cancel event, for example:
+
+ <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
+ This is another modal.
+
+
+ """
+ attr :id, :string, required: true
+ attr :show, :boolean, default: false
+ attr :on_cancel, JS, default: %JS{}
+ slot :inner_block, required: true
+
+ def modal(assigns) do
+ ~H"""
+
+
+
+
+
+ <.focus_wrap
+ id={"#{@id}-container"}
+ phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
+ phx-key="escape"
+ phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
+ class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
+ >
+
+
+ <.icon name="hero-x-mark-solid" class="h-5 w-5" />
+
+
+
+ <%= render_slot(@inner_block) %>
+
+
+
+
+
+
+ """
+ end
+
+ @doc """
+ Renders flash notices.
+
+ ## Examples
+
+ <.flash kind={:info} flash={@flash} />
+ <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
+ """
+ attr :id, :string, doc: "the optional id of flash container"
+ attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
+ attr :title, :string, default: nil
+ attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
+ attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
+
+ slot :inner_block, doc: "the optional inner block that renders the flash message"
+
+ def flash(assigns) do
+ assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
+
+ ~H"""
+ hide("##{@id}")}
+ role="alert"
+ class={[
+ "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
+ @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
+ @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
+ ]}
+ {@rest}
+ >
+
+ <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
+ <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
+ <%= @title %>
+
+
<%= msg %>
+
+ <.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
+
+
+ """
+ end
+
+ @doc """
+ Shows the flash group with standard titles and content.
+
+ ## Examples
+
+ <.flash_group flash={@flash} />
+ """
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+ attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
+
+ def flash_group(assigns) do
+ ~H"""
+
+ <.flash kind={:info} title={gettext("Success!")} flash={@flash} />
+ <.flash kind={:error} title={gettext("Error!")} flash={@flash} />
+ <.flash
+ id="client-error"
+ kind={:error}
+ title={gettext("We can't find the internet")}
+ phx-disconnected={show(".phx-client-error #client-error")}
+ phx-connected={hide("#client-error")}
+ hidden
+ >
+ <%= gettext("Attempting to reconnect") %>
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+
+ <.flash
+ id="server-error"
+ kind={:error}
+ title={gettext("Something went wrong!")}
+ phx-disconnected={show(".phx-server-error #server-error")}
+ phx-connected={hide("#server-error")}
+ hidden
+ >
+ <%= gettext("Hang in there while we get back on track") %>
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+
+ """
+ end
+
+ @doc """
+ Renders a simple form.
+
+ ## Examples
+
+ <.simple_form for={@form} phx-change="validate" phx-submit="save">
+ <.input field={@form[:email]} label="Email"/>
+ <.input field={@form[:username]} label="Username" />
+ <:actions>
+ <.button>Save
+
+
+ """
+ attr :for, :any, required: true, doc: "the datastructure for the form"
+ attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+
+ attr :rest, :global,
+ include: ~w(autocomplete name rel action enctype method novalidate target multipart),
+ doc: "the arbitrary HTML attributes to apply to the form tag"
+
+ slot :inner_block, required: true
+ slot :actions, doc: "the slot for form actions, such as a submit button"
+
+ def simple_form(assigns) do
+ ~H"""
+ <.form :let={f} for={@for} as={@as} {@rest}>
+
+ <%= render_slot(@inner_block, f) %>
+
+ <%= render_slot(action, f) %>
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a button.
+
+ ## Examples
+
+ <.button>Send!
+ <.button phx-click="go" class="ml-2">Send!
+ """
+ attr :type, :string, default: nil
+ attr :class, :string, default: nil
+ attr :rest, :global, include: ~w(disabled form name value)
+
+ slot :inner_block, required: true
+
+ def button(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Renders an input with label and error messages.
+
+ A `Phoenix.HTML.FormField` may be passed as argument,
+ which is used to retrieve the input name, id, and values.
+ Otherwise all attributes may be passed explicitly.
+
+ ## Types
+
+ This function accepts all HTML input types, considering that:
+
+ * You may also set `type="select"` to render a `` tag
+
+ * `type="checkbox"` is used exclusively to render boolean values
+
+ * For live file uploads, see `Phoenix.Component.live_file_input/1`
+
+ See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
+ for more information.
+
+ ## Examples
+
+ <.input field={@form[:email]} type="email" />
+ <.input name="my-input" errors={["oh no!"]} />
+ """
+ attr :id, :any, default: nil
+ attr :name, :any
+ attr :label, :string, default: nil
+ attr :value, :any
+
+ attr :type, :string,
+ default: "text",
+ values: ~w(checkbox color date datetime-local email file hidden month number password
+ range radio search select tel text textarea time url week)
+
+ attr :field, Phoenix.HTML.FormField,
+ doc: "a form field struct retrieved from the form, for example: @form[:email]"
+
+ attr :errors, :list, default: []
+ attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
+ attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
+ attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
+ attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
+
+ attr :rest, :global,
+ include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
+ multiple pattern placeholder readonly required rows size step)
+
+ slot :inner_block
+
+ def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
+ assigns
+ |> assign(field: nil, id: assigns.id || field.id)
+ |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
+ |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
+ |> assign_new(:value, fn -> field.value end)
+ |> input()
+ end
+
+ def input(%{type: "checkbox"} = assigns) do
+ assigns =
+ assign_new(assigns, :checked, fn ->
+ Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
+ end)
+
+ ~H"""
+
+
+
+
+ <%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ def input(%{type: "select"} = assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <%= @prompt %>
+ <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ def input(%{type: "textarea"} = assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ # All other inputs text, datetime-local, url, password, etc. are handled here...
+ def input(assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ @doc """
+ Renders a label.
+ """
+ attr :for, :string, default: nil
+ slot :inner_block, required: true
+
+ def label(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Generates a generic error message.
+ """
+ slot :inner_block, required: true
+
+ def error(assigns) do
+ ~H"""
+
+ <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Renders a header with title.
+ """
+ attr :class, :string, default: nil
+
+ slot :inner_block, required: true
+ slot :subtitle
+ slot :actions
+
+ def header(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc ~S"""
+ Renders a table with generic styling.
+
+ ## Examples
+
+ <.table id="users" rows={@users}>
+ <:col :let={user} label="id"><%= user.id %>
+ <:col :let={user} label="username"><%= user.username %>
+
+ """
+ attr :id, :string, required: true
+ attr :rows, :list, required: true
+ attr :row_id, :any, default: nil, doc: "the function for generating the row id"
+ attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
+
+ attr :row_item, :any,
+ default: &Function.identity/1,
+ doc: "the function for mapping each row before calling the :col and :action slots"
+
+ slot :col, required: true do
+ attr :label, :string
+ end
+
+ slot :action, doc: "the slot for showing user actions in the last table column"
+
+ def table(assigns) do
+ assigns =
+ with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
+ assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
+ end
+
+ ~H"""
+
+
+
+
+ <%= col[:label] %>
+
+ <%= gettext("Actions") %>
+
+
+
+
+
+
+
+
+
+ <%= render_slot(col, @row_item.(row)) %>
+
+
+
+
+
+
+
+ <%= render_slot(action, @row_item.(row)) %>
+
+
+
+
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a data list.
+
+ ## Examples
+
+ <.list>
+ <:item title="Title"><%= @post.title %>
+ <:item title="Views"><%= @post.views %>
+
+ """
+ slot :item, required: true do
+ attr :title, :string, required: true
+ end
+
+ def list(assigns) do
+ ~H"""
+
+
+
+
<%= item.title %>
+ <%= render_slot(item) %>
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a back navigation link.
+
+ ## Examples
+
+ <.back navigate={~p"/posts"}>Back to posts
+ """
+ attr :navigate, :any, required: true
+ slot :inner_block, required: true
+
+ def back(assigns) do
+ ~H"""
+
+ <.link
+ navigate={@navigate}
+ class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
+ >
+ <.icon name="hero-arrow-left-solid" class="h-3 w-3" />
+ <%= render_slot(@inner_block) %>
+
+
+ """
+ end
+
+ @doc """
+ Renders a [Heroicon](https://heroicons.com).
+
+ Heroicons come in three styles – outline, solid, and mini.
+ By default, the outline style is used, but solid and mini may
+ be applied by using the `-solid` and `-mini` suffix.
+
+ You can customize the size and colors of the icons by setting
+ width, height, and background color classes.
+
+ Icons are extracted from the `deps/heroicons` directory and bundled within
+ your compiled app.css by the plugin in your `assets/tailwind.config.js`.
+
+ ## Examples
+
+ <.icon name="hero-x-mark-solid" />
+ <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
+ """
+ attr :name, :string, required: true
+ attr :class, :string, default: nil
+
+ def icon(%{name: "hero-" <> _} = assigns) do
+ ~H"""
+
+ """
+ end
+
+ ## JS Commands
+
+ def show(js \\ %JS{}, selector) do
+ JS.show(js,
+ to: selector,
+ transition:
+ {"transition-all transform ease-out duration-300",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
+ "opacity-100 translate-y-0 sm:scale-100"}
+ )
+ end
+
+ def hide(js \\ %JS{}, selector) do
+ JS.hide(js,
+ to: selector,
+ time: 200,
+ transition:
+ {"transition-all transform ease-in duration-200",
+ "opacity-100 translate-y-0 sm:scale-100",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
+ )
+ end
+
+ def show_modal(js \\ %JS{}, id) when is_binary(id) do
+ js
+ |> JS.show(to: "##{id}")
+ |> JS.show(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
+ )
+ |> show("##{id}-container")
+ |> JS.add_class("overflow-hidden", to: "body")
+ |> JS.focus_first(to: "##{id}-content")
+ end
+
+ def hide_modal(js \\ %JS{}, id) do
+ js
+ |> JS.hide(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
+ )
+ |> hide("##{id}-container")
+ |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
+ |> JS.remove_class("overflow-hidden", to: "body")
+ |> JS.pop_focus()
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # When using gettext, we typically pass the strings we want
+ # to translate as a static argument:
+ #
+ # # Translate the number of files with plural rules
+ # dngettext("errors", "1 file", "%{count} files", count)
+ #
+ # However the error messages in our forms and APIs are generated
+ # dynamically, so we need to translate them by calling Gettext
+ # with our gettext backend as first argument. Translations are
+ # available in the errors.po file (as we use the "errors" domain).
+ if count = opts[:count] do
+ Gettext.dngettext(FlixWeb.Gettext, "errors", msg, msg, count, opts)
+ else
+ Gettext.dgettext(FlixWeb.Gettext, "errors", msg, opts)
+ end
+ end
+
+ @doc """
+ Translates the errors for a field from a keyword list of errors.
+ """
+ def translate_errors(errors, field) when is_list(errors) do
+ for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
+ end
+end
diff --git a/lib/flix_web/components/layouts.ex b/lib/flix_web/components/layouts.ex
new file mode 100644
index 0000000..c0df8cd
--- /dev/null
+++ b/lib/flix_web/components/layouts.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.Layouts do
+ use FlixWeb, :html
+
+ embed_templates "layouts/*"
+end
diff --git a/lib/flix_web/components/layouts/app.html.heex b/lib/flix_web/components/layouts/app.html.heex
new file mode 100644
index 0000000..e23bfc8
--- /dev/null
+++ b/lib/flix_web/components/layouts/app.html.heex
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ v<%= Application.spec(:phoenix, :vsn) %>
+
+
+
+
+
+
+
+ <.flash_group flash={@flash} />
+ <%= @inner_content %>
+
+
diff --git a/lib/flix_web/components/layouts/root.html.heex b/lib/flix_web/components/layouts/root.html.heex
new file mode 100644
index 0000000..b6fdf37
--- /dev/null
+++ b/lib/flix_web/components/layouts/root.html.heex
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ <.live_title suffix=" · Phoenix Framework">
+ <%= assigns[:page_title] || "Flix" %>
+
+
+
+
+
+ <%= @inner_content %>
+
+
diff --git a/lib/flix_web/templates/email/confirmation_instructions.html.heex b/lib/flix_web/controllers/email_html/confirmation_instructions.html.heex
similarity index 100%
rename from lib/flix_web/templates/email/confirmation_instructions.html.heex
rename to lib/flix_web/controllers/email_html/confirmation_instructions.html.heex
diff --git a/lib/flix_web/templates/email/confirmation_instructions.text.heex b/lib/flix_web/controllers/email_html/confirmation_instructions.text.heex
similarity index 100%
rename from lib/flix_web/templates/email/confirmation_instructions.text.heex
rename to lib/flix_web/controllers/email_html/confirmation_instructions.text.heex
diff --git a/lib/flix_web/templates/email/reset_password_instructions.html.heex b/lib/flix_web/controllers/email_html/reset_password_instructions.html.heex
similarity index 100%
rename from lib/flix_web/templates/email/reset_password_instructions.html.heex
rename to lib/flix_web/controllers/email_html/reset_password_instructions.html.heex
diff --git a/lib/flix_web/templates/email/reset_password_instructions.text.heex b/lib/flix_web/controllers/email_html/reset_password_instructions.text.heex
similarity index 100%
rename from lib/flix_web/templates/email/reset_password_instructions.text.heex
rename to lib/flix_web/controllers/email_html/reset_password_instructions.text.heex
diff --git a/lib/flix_web/templates/email/update_email_instructions.html.heex b/lib/flix_web/controllers/email_html/update_email_instructions.html.heex
similarity index 100%
rename from lib/flix_web/templates/email/update_email_instructions.html.heex
rename to lib/flix_web/controllers/email_html/update_email_instructions.html.heex
diff --git a/lib/flix_web/templates/email/update_email_instructions.text.heex b/lib/flix_web/controllers/email_html/update_email_instructions.text.heex
similarity index 100%
rename from lib/flix_web/templates/email/update_email_instructions.text.heex
rename to lib/flix_web/controllers/email_html/update_email_instructions.text.heex
diff --git a/lib/flix_web/controllers/fan_html.ex b/lib/flix_web/controllers/fan_html.ex
new file mode 100644
index 0000000..42fa490
--- /dev/null
+++ b/lib/flix_web/controllers/fan_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.FanHTML do
+ use FlixWeb, :html
+
+ embed_templates "fan_html/*"
+end
diff --git a/lib/flix_web/templates/fan/index.html.heex b/lib/flix_web/controllers/fan_html/index.html.heex
similarity index 100%
rename from lib/flix_web/templates/fan/index.html.heex
rename to lib/flix_web/controllers/fan_html/index.html.heex
diff --git a/lib/flix_web/templates/fan/show.html.heex b/lib/flix_web/controllers/fan_html/show.html.heex
similarity index 100%
rename from lib/flix_web/templates/fan/show.html.heex
rename to lib/flix_web/controllers/fan_html/show.html.heex
diff --git a/lib/flix_web/controllers/favorite_html.ex b/lib/flix_web/controllers/favorite_html.ex
new file mode 100644
index 0000000..f2d1631
--- /dev/null
+++ b/lib/flix_web/controllers/favorite_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.FavoriteHTML do
+ use FlixWeb, :html
+
+ embed_templates "favorite_html/*"
+end
diff --git a/lib/flix_web/controllers/genre_html.ex b/lib/flix_web/controllers/genre_html.ex
new file mode 100644
index 0000000..26c7ee6
--- /dev/null
+++ b/lib/flix_web/controllers/genre_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.GenreHTML do
+ use FlixWeb, :html
+
+ embed_templates "genre_html/*"
+end
diff --git a/lib/flix_web/templates/genre/edit.html.heex b/lib/flix_web/controllers/genre_html/edit.html.heex
similarity index 100%
rename from lib/flix_web/templates/genre/edit.html.heex
rename to lib/flix_web/controllers/genre_html/edit.html.heex
diff --git a/lib/flix_web/templates/genre/form.html.heex b/lib/flix_web/controllers/genre_html/form.html.heex
similarity index 100%
rename from lib/flix_web/templates/genre/form.html.heex
rename to lib/flix_web/controllers/genre_html/form.html.heex
diff --git a/lib/flix_web/templates/genre/index.html.heex b/lib/flix_web/controllers/genre_html/index.html.heex
similarity index 100%
rename from lib/flix_web/templates/genre/index.html.heex
rename to lib/flix_web/controllers/genre_html/index.html.heex
diff --git a/lib/flix_web/templates/genre/new.html.heex b/lib/flix_web/controllers/genre_html/new.html.heex
similarity index 100%
rename from lib/flix_web/templates/genre/new.html.heex
rename to lib/flix_web/controllers/genre_html/new.html.heex
diff --git a/lib/flix_web/templates/genre/show.html.heex b/lib/flix_web/controllers/genre_html/show.html.heex
similarity index 100%
rename from lib/flix_web/templates/genre/show.html.heex
rename to lib/flix_web/controllers/genre_html/show.html.heex
diff --git a/lib/flix_web/controllers/movie_html.ex b/lib/flix_web/controllers/movie_html.ex
new file mode 100644
index 0000000..d1fe41c
--- /dev/null
+++ b/lib/flix_web/controllers/movie_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.MovieHTML do
+ use FlixWeb, :html
+
+ embed_templates "movie_html/*"
+end
diff --git a/lib/flix_web/templates/movie/edit.html.heex b/lib/flix_web/controllers/movie_html/edit.html.heex
similarity index 100%
rename from lib/flix_web/templates/movie/edit.html.heex
rename to lib/flix_web/controllers/movie_html/edit.html.heex
diff --git a/lib/flix_web/templates/movie/form.html.heex b/lib/flix_web/controllers/movie_html/form.html.heex
similarity index 100%
rename from lib/flix_web/templates/movie/form.html.heex
rename to lib/flix_web/controllers/movie_html/form.html.heex
diff --git a/lib/flix_web/templates/movie/index.html.heex b/lib/flix_web/controllers/movie_html/index.html.heex
similarity index 100%
rename from lib/flix_web/templates/movie/index.html.heex
rename to lib/flix_web/controllers/movie_html/index.html.heex
diff --git a/lib/flix_web/templates/movie/new.html.heex b/lib/flix_web/controllers/movie_html/new.html.heex
similarity index 100%
rename from lib/flix_web/templates/movie/new.html.heex
rename to lib/flix_web/controllers/movie_html/new.html.heex
diff --git a/lib/flix_web/templates/movie/show.html.heex b/lib/flix_web/controllers/movie_html/show.html.heex
similarity index 100%
rename from lib/flix_web/templates/movie/show.html.heex
rename to lib/flix_web/controllers/movie_html/show.html.heex
diff --git a/lib/flix_web/controllers/page_controller.ex b/lib/flix_web/controllers/page_controller.ex
index fc413cc..9cd4fb1 100644
--- a/lib/flix_web/controllers/page_controller.ex
+++ b/lib/flix_web/controllers/page_controller.ex
@@ -1,7 +1,9 @@
defmodule FlixWeb.PageController do
use FlixWeb, :controller
- def index(conn, _params) do
- render(conn, "index.html")
+ def home(conn, _params) do
+ # The home page is often custom made,
+ # so skip the default app layout.
+ render(conn, :home, layout: false)
end
end
diff --git a/lib/flix_web/controllers/page_html.ex b/lib/flix_web/controllers/page_html.ex
new file mode 100644
index 0000000..8046be0
--- /dev/null
+++ b/lib/flix_web/controllers/page_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.PageHTML do
+ use FlixWeb, :html
+
+ embed_templates "page_html/*"
+end
diff --git a/lib/flix_web/controllers/page_html/home.html.heex b/lib/flix_web/controllers/page_html/home.html.heex
new file mode 100644
index 0000000..dc1820b
--- /dev/null
+++ b/lib/flix_web/controllers/page_html/home.html.heex
@@ -0,0 +1,222 @@
+<.flash_group flash={@flash} />
+
+
+
+
+
+
+
+ Phoenix Framework
+
+ v<%= Application.spec(:phoenix, :vsn) %>
+
+
+
+ Peace of mind from prototype to production.
+
+
+ Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
+
+
+
+
diff --git a/lib/flix_web/controllers/review_html.ex b/lib/flix_web/controllers/review_html.ex
new file mode 100644
index 0000000..42fa490
--- /dev/null
+++ b/lib/flix_web/controllers/review_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.FanHTML do
+ use FlixWeb, :html
+
+ embed_templates "fan_html/*"
+end
diff --git a/lib/flix_web/templates/review/edit.html.heex b/lib/flix_web/controllers/review_html/edit.html.heex
similarity index 100%
rename from lib/flix_web/templates/review/edit.html.heex
rename to lib/flix_web/controllers/review_html/edit.html.heex
diff --git a/lib/flix_web/templates/review/form.html.heex b/lib/flix_web/controllers/review_html/form.html.heex
similarity index 100%
rename from lib/flix_web/templates/review/form.html.heex
rename to lib/flix_web/controllers/review_html/form.html.heex
diff --git a/lib/flix_web/templates/review/index.html.heex b/lib/flix_web/controllers/review_html/index.html.heex
similarity index 100%
rename from lib/flix_web/templates/review/index.html.heex
rename to lib/flix_web/controllers/review_html/index.html.heex
diff --git a/lib/flix_web/templates/review/new.html.heex b/lib/flix_web/controllers/review_html/new.html.heex
similarity index 100%
rename from lib/flix_web/templates/review/new.html.heex
rename to lib/flix_web/controllers/review_html/new.html.heex
diff --git a/lib/flix_web/templates/shared/_errors.html.heex b/lib/flix_web/controllers/shared_html/_errors.html.heex
similarity index 100%
rename from lib/flix_web/templates/shared/_errors.html.heex
rename to lib/flix_web/controllers/shared_html/_errors.html.heex
diff --git a/lib/flix_web/templates/shared/_stars.html.heex b/lib/flix_web/controllers/shared_html/_stars.html.heex
similarity index 100%
rename from lib/flix_web/templates/shared/_stars.html.heex
rename to lib/flix_web/controllers/shared_html/_stars.html.heex
diff --git a/lib/flix_web/controllers/user_confirmation_controller.ex b/lib/flix_web/controllers/user_confirmation_controller.ex
index 7d5723d..e47f307 100644
--- a/lib/flix_web/controllers/user_confirmation_controller.ex
+++ b/lib/flix_web/controllers/user_confirmation_controller.ex
@@ -4,35 +4,38 @@ defmodule FlixWeb.UserConfirmationController do
alias Flix.Accounts
def new(conn, _params) do
- render(conn, "new.html")
+ render(conn, :new)
end
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_confirmation_instructions(
user,
- &Routes.user_confirmation_url(conn, :confirm, &1)
+ &url(~p"/users/confirm/#{&1}")
)
end
- # Regardless of the outcome, show an impartial success/error message.
conn
|> put_flash(
- :notice,
+ :info,
"If your email is in our system and it has not been confirmed yet, " <>
"you will receive an email with instructions shortly."
)
- |> redirect(to: "/")
+ |> redirect(to: ~p"/")
+ end
+
+ def edit(conn, %{"token" => token}) do
+ render(conn, :edit, token: token)
end
# Do not log in the user after confirmation to avoid a
# leaked token giving the user access to the account.
- def confirm(conn, %{"token" => token}) do
+ def update(conn, %{"token" => token}) do
case Accounts.confirm_user(token) do
{:ok, _} ->
conn
- |> put_flash(:notice, "Account confirmed successfully.")
- |> redirect(to: "/")
+ |> put_flash(:info, "User confirmed successfully.")
+ |> redirect(to: ~p"/")
:error ->
# If there is a current user and the account was already confirmed,
@@ -41,12 +44,12 @@ defmodule FlixWeb.UserConfirmationController do
# a warning message.
case conn.assigns do
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
- redirect(conn, to: "/")
+ redirect(conn, to: ~p"/")
%{} ->
conn
- |> put_flash(:error, "Account confirmation link is invalid or it has expired.")
- |> redirect(to: "/")
+ |> put_flash(:error, "User confirmation link is invalid or it has expired.")
+ |> redirect(to: ~p"/")
end
end
end
diff --git a/lib/flix_web/controllers/user_confirmation_html.ex b/lib/flix_web/controllers/user_confirmation_html.ex
new file mode 100644
index 0000000..656bc2f
--- /dev/null
+++ b/lib/flix_web/controllers/user_confirmation_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.UserConfirmationHTML do
+ use FlixWeb, :html
+
+ embed_templates "user_confirmation_html/*"
+end
diff --git a/lib/flix_web/controllers/user_confirmation_html/edit.html.heex b/lib/flix_web/controllers/user_confirmation_html/edit.html.heex
new file mode 100644
index 0000000..f320cf7
--- /dev/null
+++ b/lib/flix_web/controllers/user_confirmation_html/edit.html.heex
@@ -0,0 +1,14 @@
+
+ <.header class="text-center">Confirm account
+
+ <.simple_form for={@conn.params["user"]} as={:user} action={~p"/users/confirm/#{@token}"}>
+ <:actions>
+ <.button class="w-full">Confirm my account
+
+
+
+
+ <.link href={~p"/users/register"}>Register
+ | <.link href={~p"/users/log_in"}>Log in
+
+
diff --git a/lib/flix_web/controllers/user_confirmation_html/new.html.heex b/lib/flix_web/controllers/user_confirmation_html/new.html.heex
new file mode 100644
index 0000000..8db6796
--- /dev/null
+++ b/lib/flix_web/controllers/user_confirmation_html/new.html.heex
@@ -0,0 +1,21 @@
+
+ <.header class="text-center">
+ No confirmation instructions received?
+ <:subtitle>We'll send a new confirmation link to your inbox
+
+
+ <.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/users/confirm"}>
+ <.input field={f[:email]} type="email" placeholder="Email" required />
+ <:actions>
+ <.button phx-disable-with="Sending..." class="w-full">
+ Resend confirmation instructions
+
+
+
+
+
+ <.link href={~p"/users/register"}>Register
+ |
+ <.link href={~p"/users/log_in"}>Log in
+
+
diff --git a/lib/flix_web/controllers/user_registration_controller.ex b/lib/flix_web/controllers/user_registration_controller.ex
index 6b3909b..37cd44d 100644
--- a/lib/flix_web/controllers/user_registration_controller.ex
+++ b/lib/flix_web/controllers/user_registration_controller.ex
@@ -7,7 +7,7 @@ defmodule FlixWeb.UserRegistrationController do
def new(conn, _params) do
changeset = Accounts.change_user_registration(%User{})
- render(conn, "new.html", changeset: changeset)
+ render(conn, :new, changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
@@ -16,15 +16,15 @@ defmodule FlixWeb.UserRegistrationController do
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
- &Routes.user_confirmation_url(conn, :confirm, &1)
+ &url(~p"/users/confirm/#{&1}")
)
conn
- |> put_flash(:notice, "User created successfully.")
+ |> put_flash(:info, "User created successfully.")
|> UserAuth.log_in_user(user)
{:error, %Ecto.Changeset{} = changeset} ->
- render(conn, "new.html", changeset: changeset)
+ render(conn, :new, changeset: changeset)
end
end
end
diff --git a/lib/flix_web/controllers/user_registration_html.ex b/lib/flix_web/controllers/user_registration_html.ex
new file mode 100644
index 0000000..8eed41d
--- /dev/null
+++ b/lib/flix_web/controllers/user_registration_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.UserRegistrationHTML do
+ use FlixWeb, :html
+
+ embed_templates "user_registration_html/*"
+end
diff --git a/lib/flix_web/controllers/user_registration_html/new.html.heex b/lib/flix_web/controllers/user_registration_html/new.html.heex
new file mode 100644
index 0000000..001052c
--- /dev/null
+++ b/lib/flix_web/controllers/user_registration_html/new.html.heex
@@ -0,0 +1,26 @@
+
+ <.header class="text-center">
+ Register for an account
+ <:subtitle>
+ Already registered?
+ <.link navigate={~p"/users/log_in"} class="font-semibold text-brand hover:underline">
+ Sign in
+
+ to your account now.
+
+
+
+ <.simple_form :let={f} for={@changeset} action={~p"/users/register"}>
+ <.error :if={@changeset.action == :insert}>
+ Oops, something went wrong! Please check the errors below.
+
+
+ <.input field={f[:name]} type="text" label="Name" required="true" autofocus="true" />
+ <.input field={f[:email]} type="email" label="Email" required="true" />
+ <.input field={f[:username]} type="text" label="Username" required="true" />
+ <.input field={f[:password]} type="password" label="Password" required="true" />
+ <:actions>
+ <.button phx-disable-with="Creating account..." class="w-full">Create an account
+
+
+
diff --git a/lib/flix_web/controllers/user_reset_password_controller.ex b/lib/flix_web/controllers/user_reset_password_controller.ex
index 00ae171..3fd80c6 100644
--- a/lib/flix_web/controllers/user_reset_password_controller.ex
+++ b/lib/flix_web/controllers/user_reset_password_controller.ex
@@ -6,28 +6,27 @@ defmodule FlixWeb.UserResetPasswordController do
plug :get_user_by_reset_password_token when action in [:edit, :update]
def new(conn, _params) do
- render(conn, "new.html")
+ render(conn, :new)
end
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_reset_password_instructions(
user,
- &Routes.user_reset_password_url(conn, :edit, &1)
+ &url(~p"/users/reset_password/#{&1}")
)
end
- # Regardless of the outcome, show an impartial success/error message.
conn
|> put_flash(
- :notice,
+ :info,
"If your email is in our system, you will receive instructions to reset your password shortly."
)
- |> redirect(to: "/")
+ |> redirect(to: ~p"/")
end
def edit(conn, _params) do
- render(conn, "edit.html", changeset: Accounts.change_user_password(conn.assigns.user))
+ render(conn, :edit, changeset: Accounts.change_user_password(conn.assigns.user))
end
# Do not log in the user after reset password to avoid a
@@ -36,11 +35,11 @@ defmodule FlixWeb.UserResetPasswordController do
case Accounts.reset_user_password(conn.assigns.user, user_params) do
{:ok, _} ->
conn
- |> put_flash(:notice, "Password reset successfully.")
- |> redirect(to: Routes.user_session_path(conn, :new))
+ |> put_flash(:info, "Password reset successfully.")
+ |> redirect(to: ~p"/users/log_in")
{:error, changeset} ->
- render(conn, "edit.html", changeset: changeset)
+ render(conn, :edit, changeset: changeset)
end
end
@@ -52,7 +51,7 @@ defmodule FlixWeb.UserResetPasswordController do
else
conn
|> put_flash(:error, "Reset password link is invalid or it has expired.")
- |> redirect(to: "/")
+ |> redirect(to: ~p"/")
|> halt()
end
end
diff --git a/lib/flix_web/controllers/user_reset_password_html.ex b/lib/flix_web/controllers/user_reset_password_html.ex
new file mode 100644
index 0000000..a24446a
--- /dev/null
+++ b/lib/flix_web/controllers/user_reset_password_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.UserResetPasswordHTML do
+ use FlixWeb, :html
+
+ embed_templates "user_reset_password_html/*"
+end
diff --git a/lib/flix_web/controllers/user_reset_password_html/edit.html.heex b/lib/flix_web/controllers/user_reset_password_html/edit.html.heex
new file mode 100644
index 0000000..cd02639
--- /dev/null
+++ b/lib/flix_web/controllers/user_reset_password_html/edit.html.heex
@@ -0,0 +1,30 @@
+
+ <.header class="text-center">
+ Reset Password
+
+
+ <.simple_form :let={f} for={@changeset} action={~p"/users/reset_password/#{@token}"}>
+ <.error :if={@changeset.action}>
+ Oops, something went wrong! Please check the errors below.
+
+
+ <.input field={f[:password]} type="password" label="New Password" required />
+ <.input
+ field={f[:password_confirmation]}
+ type="password"
+ label="Confirm new password"
+ required
+ />
+ <:actions>
+ <.button phx-disable-with="Resetting..." class="w-full">
+ Reset password
+
+
+
+
+
+ <.link href={~p"/users/register"}>Register
+ |
+ <.link href={~p"/users/log_in"}>Log in
+
+
diff --git a/lib/flix_web/controllers/user_reset_password_html/new.html.heex b/lib/flix_web/controllers/user_reset_password_html/new.html.heex
new file mode 100644
index 0000000..e41d9c6
--- /dev/null
+++ b/lib/flix_web/controllers/user_reset_password_html/new.html.heex
@@ -0,0 +1,21 @@
+
+ <.header class="text-center">
+ Forgot your password?
+ <:subtitle>We'll send a password reset link to your inbox
+
+
+ <.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/users/reset_password"}>
+ <.input field={f[:email]} type="email" placeholder="Email" required />
+ <:actions>
+ <.button phx-disable-with="Sending..." class="w-full">
+ Send password reset instructions
+
+
+
+
+
+ <.link href={~p"/users/register"}>Register
+ |
+ <.link href={~p"/users/log_in"}>Log in
+
+
diff --git a/lib/flix_web/controllers/user_session_controller.ex b/lib/flix_web/controllers/user_session_controller.ex
index 607eee3..d5898c6 100644
--- a/lib/flix_web/controllers/user_session_controller.ex
+++ b/lib/flix_web/controllers/user_session_controller.ex
@@ -5,7 +5,7 @@ defmodule FlixWeb.UserSessionController do
alias FlixWeb.UserAuth
def new(conn, _params) do
- render(conn, "new.html", error_message: nil)
+ render(conn, :new, error_message: nil)
end
def create(conn, %{"user" => user_params}) do
@@ -13,16 +13,17 @@ defmodule FlixWeb.UserSessionController do
if user = Accounts.get_user_by_email_and_password(email, password) do
conn
- |> put_flash(:notice, "Welcome back, #{user.name}!")
+ |> put_flash(:info, "Welcome back!")
|> UserAuth.log_in_user(user, user_params)
else
- render(conn, "new.html", error_message: "Invalid email/password combination!")
+ # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
+ render(conn, :new, error_message: "Invalid email or password")
end
end
def delete(conn, _params) do
conn
- |> put_flash(:notice, "You're now signed out!")
+ |> put_flash(:info, "Logged out successfully.")
|> UserAuth.log_out_user()
end
end
diff --git a/lib/flix_web/controllers/user_session_html.ex b/lib/flix_web/controllers/user_session_html.ex
new file mode 100644
index 0000000..b0a9985
--- /dev/null
+++ b/lib/flix_web/controllers/user_session_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.UserSessionHTML do
+ use FlixWeb, :html
+
+ embed_templates "user_session_html/*"
+end
diff --git a/lib/flix_web/controllers/user_session_html/new.html.heex b/lib/flix_web/controllers/user_session_html/new.html.heex
new file mode 100644
index 0000000..fa23550
--- /dev/null
+++ b/lib/flix_web/controllers/user_session_html/new.html.heex
@@ -0,0 +1,30 @@
+
+ <.header class="text-center">
+ Sign in to account
+ <:subtitle>
+ Don't have an account?
+ <.link navigate={~p"/users/register"} class="font-semibold text-brand hover:underline">
+ Sign up
+
+ for an account now.
+
+
+
+ <.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/users/log_in"}>
+ <.error :if={@error_message}><%= @error_message %>
+
+ <.input field={f[:email]} type="email" label="Email" required />
+ <.input field={f[:password]} type="password" label="Password" required />
+ <:actions :let={f}>
+ <.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" />
+ <.link href={~p"/users/reset_password"} class="text-sm font-semibold">
+ Forgot your password?
+
+
+ <:actions>
+ <.button phx-disable-with="Signing in..." class="w-full">
+ Sign in →
+
+
+
+
diff --git a/lib/flix_web/controllers/user_settings_controller.ex b/lib/flix_web/controllers/user_settings_controller.ex
index 757f646..b983662 100644
--- a/lib/flix_web/controllers/user_settings_controller.ex
+++ b/lib/flix_web/controllers/user_settings_controller.ex
@@ -7,7 +7,7 @@ defmodule FlixWeb.UserSettingsController do
plug :assign_email_and_password_changesets
def edit(conn, _params) do
- render(conn, "edit.html")
+ render(conn, :edit)
end
def update(conn, %{"action" => "update_email"} = params) do
@@ -16,21 +16,21 @@ defmodule FlixWeb.UserSettingsController do
case Accounts.apply_user_email(user, password, user_params) do
{:ok, applied_user} ->
- Accounts.deliver_update_email_instructions(
+ Accounts.deliver_user_update_email_instructions(
applied_user,
user.email,
- &Routes.user_settings_url(conn, :confirm_email, &1)
+ &url(~p"/users/settings/confirm_email/#{&1}")
)
conn
|> put_flash(
- :notice,
+ :info,
"A link to confirm your email change has been sent to the new address."
)
- |> redirect(to: Routes.user_settings_path(conn, :edit))
+ |> redirect(to: ~p"/users/settings")
{:error, changeset} ->
- render(conn, "edit.html", email_changeset: changeset)
+ render(conn, :edit, email_changeset: changeset)
end
end
@@ -41,12 +41,12 @@ defmodule FlixWeb.UserSettingsController do
case Accounts.update_user_password(user, password, user_params) do
{:ok, user} ->
conn
- |> put_flash(:notice, "Password updated successfully.")
- |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
+ |> put_flash(:info, "Password updated successfully.")
+ |> put_session(:user_return_to, ~p"/users/settings")
|> UserAuth.log_in_user(user)
{:error, changeset} ->
- render(conn, "edit.html", password_changeset: changeset)
+ render(conn, :edit, password_changeset: changeset)
end
end
@@ -54,29 +54,13 @@ defmodule FlixWeb.UserSettingsController do
case Accounts.update_user_email(conn.assigns.current_user, token) do
:ok ->
conn
- |> put_flash(:notice, "Email changed successfully.")
- |> redirect(to: Routes.user_settings_path(conn, :edit))
+ |> put_flash(:info, "Email changed successfully.")
+ |> redirect(to: ~p"/users/settings")
:error ->
conn
|> put_flash(:error, "Email change link is invalid or it has expired.")
- |> redirect(to: Routes.user_settings_path(conn, :edit))
- end
- end
-
- def delete(conn, _params) do
- user = conn.assigns.current_user
-
- case Accounts.unregister_user(user.id) do
- {:ok, _user} ->
- conn
- |> put_flash(:notice, "Sorry to hear that you're deleting your acount.")
- |> redirect(to: Routes.movie_path(conn, :index))
-
- {:error, _error} ->
- conn
- |> put_flash(:error, "There was a problem deleting your account. Please try again.")
- |> redirect(to: Routes.movie_path(conn, :index))
+ |> redirect(to: ~p"/users/settings")
end
end
diff --git a/lib/flix_web/controllers/user_settings_html.ex b/lib/flix_web/controllers/user_settings_html.ex
new file mode 100644
index 0000000..2e0b1a5
--- /dev/null
+++ b/lib/flix_web/controllers/user_settings_html.ex
@@ -0,0 +1,5 @@
+defmodule FlixWeb.UserSettingsHTML do
+ use FlixWeb, :html
+
+ embed_templates "user_settings_html/*"
+end
diff --git a/lib/flix_web/controllers/user_settings_html/edit.html.heex b/lib/flix_web/controllers/user_settings_html/edit.html.heex
new file mode 100644
index 0000000..915c3fd
--- /dev/null
+++ b/lib/flix_web/controllers/user_settings_html/edit.html.heex
@@ -0,0 +1,63 @@
+<.header class="text-center">
+ Account Settings
+ <:subtitle>Manage your account email address and password settings
+
+
+
+
+ <.simple_form :let={f} for={@email_changeset} action={~p"/users/settings"} id="update_email">
+ <.error :if={@email_changeset.action}>
+ Oops, something went wrong! Please check the errors below.
+
+
+ <.input field={f[:action]} type="hidden" name="action" value="update_email" />
+
+ <.input field={f[:email]} type="email" label="Email" required />
+ <.input
+ field={f[:current_password]}
+ name="current_password"
+ type="password"
+ label="Current Password"
+ required
+ id="current_password_for_email"
+ />
+ <:actions>
+ <.button phx-disable-with="Changing...">Change Email
+
+
+
+
+ <.simple_form
+ :let={f}
+ for={@password_changeset}
+ action={~p"/users/settings"}
+ id="update_password"
+ >
+ <.error :if={@password_changeset.action}>
+ Oops, something went wrong! Please check the errors below.
+
+
+ <.input field={f[:action]} type="hidden" name="action" value="update_password" />
+
+ <.input field={f[:password]} type="password" label="New password" required />
+ <.input
+ field={f[:password_confirmation]}
+ type="password"
+ label="Confirm new password"
+ required
+ />
+
+ <.input
+ field={f[:current_password]}
+ name="current_password"
+ type="password"
+ label="Current password"
+ id="current_password_for_password"
+ required
+ />
+ <:actions>
+ <.button phx-disable-with="Changing...">Change Password
+
+
+
+
diff --git a/lib/flix_web/endpoint.ex b/lib/flix_web/endpoint.ex
index 7ac53d4..2cbcc7e 100644
--- a/lib/flix_web/endpoint.ex
+++ b/lib/flix_web/endpoint.ex
@@ -7,40 +7,36 @@ defmodule FlixWeb.Endpoint do
@session_options [
store: :cookie,
key: "_flix_key",
- signing_salt: "NFzKYqak"
+ signing_salt: "MpQjjKl3",
+ same_site: "Lax"
]
- socket("/socket", FlixWeb.UserSocket,
- websocket: [timeout: 45_000],
- longpoll: false
- )
-
- socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
+ socket "/live", Phoenix.LiveView.Socket,
+ websocket: [connect_info: [session: @session_options]],
+ longpoll: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
- plug(Plug.Static,
+ plug Plug.Static,
at: "/",
from: :flix,
gzip: false,
- only: ~w(assets css fonts images js favicon.ico robots.txt)
- )
+ only: FlixWeb.static_paths()
- plug(Plug.Static,
+ plug Plug.Static,
at: "/uploads",
from: Path.expand("./uploads"),
gzip: false
- )
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
- socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
- plug(Phoenix.LiveReloader)
- plug(Phoenix.CodeReloader)
- plug(Phoenix.Ecto.CheckRepoStatus, otp_app: :flix)
+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+ plug Phoenix.LiveReloader
+ plug Phoenix.CodeReloader
+ plug Phoenix.Ecto.CheckRepoStatus, otp_app: :flix
end
plug(Phoenix.LiveDashboard.RequestLogger,
diff --git a/lib/flix_web/router.ex b/lib/flix_web/router.ex
index 08747db..65b7a9d 100644
--- a/lib/flix_web/router.ex
+++ b/lib/flix_web/router.ex
@@ -21,15 +21,15 @@ defmodule FlixWeb.Router do
if Mix.env() in [:dev, :test] do
forward "/graphiql",
- Absinthe.Plug.GraphiQL,
- schema: FlixWeb.Graphql.Schema,
- json_codec: Jason,
- interface: :playground
+ Absinthe.Plug.GraphiQL,
+ schema: FlixWeb.Graphql.Schema,
+ json_codec: Jason,
+ interface: :playground
end
forward "/api",
- Absinthe.Plug,
- schema: FlixWeb.Graphql.Schema
+ Absinthe.Plug,
+ schema: FlixWeb.Graphql.Schema
end
scope "/", FlixWeb do
@@ -55,26 +55,21 @@ defmodule FlixWeb.Router do
# pipe_through :api
# end
- # Enables LiveDashboard only for development
- #
- # 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).
- if Mix.env() in [:dev, :test] do
+ # Enable LiveDashboard and Swoosh mailbox preview in development
+ if Application.compile_env(:flix, :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 "/" do
- pipe_through(:browser)
- live_dashboard("/dashboard", metrics: FlixWeb.Telemetry)
- end
- end
+ scope "/dev" do
+ pipe_through :browser
- # Enables send e-mails views only for development
-
- if Mix.env() == :dev do
- forward "/sent_emails", Bamboo.SentEmailViewerPlug
+ live_dashboard "/dashboard", metrics: FlixWeb.Telemetry
+ forward "/mailbox", Plug.Swoosh.MailboxPreview
+ end
end
## Authentication routes
diff --git a/lib/flix_web/telemetry.ex b/lib/flix_web/telemetry.ex
index b29efb8..ce1f8b7 100644
--- a/lib/flix_web/telemetry.ex
+++ b/lib/flix_web/telemetry.ex
@@ -22,20 +22,57 @@ defmodule FlixWeb.Telemetry do
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("flix.repo.query.total_time", unit: {:native, :millisecond}),
- summary("flix.repo.query.decode_time", unit: {:native, :millisecond}),
- summary("flix.repo.query.query_time", unit: {:native, :millisecond}),
- summary("flix.repo.query.queue_time", unit: {:native, :millisecond}),
- summary("flix.repo.query.idle_time", unit: {:native, :millisecond}),
+ summary("flix.repo.query.total_time",
+ unit: {:native, :millisecond},
+ description: "The sum of the other measurements"
+ ),
+ summary("flix.repo.query.decode_time",
+ unit: {:native, :millisecond},
+ description: "The time spent decoding the data received from the database"
+ ),
+ summary("flix.repo.query.query_time",
+ unit: {:native, :millisecond},
+ description: "The time spent executing the query"
+ ),
+ summary("flix.repo.query.queue_time",
+ unit: {:native, :millisecond},
+ description: "The time spent waiting for a database connection"
+ ),
+ summary("flix.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}),
diff --git a/lib/flix_web/templates/page/index.html.heex b/lib/flix_web/templates/page/index.html.heex
deleted file mode 100644
index dd0a7f8..0000000
--- a/lib/flix_web/templates/page/index.html.heex
+++ /dev/null
@@ -1,38 +0,0 @@
-
- <%= gettext "Welcome to %{name}!", name: "Phoenix" %>
- Peace of mind from prototype to production
-
-
-
-
- Resources
-
-
-
- Help
-
-
-
diff --git a/lib/flix_web/templates/user_confirmation/new.html.heex b/lib/flix_web/templates/user_confirmation/new.html.heex
deleted file mode 100644
index 6edb889..0000000
--- a/lib/flix_web/templates/user_confirmation/new.html.heex
+++ /dev/null
@@ -1,15 +0,0 @@
-Resend confirmation instructions
-
-<%= form_for :user, Routes.user_confirmation_path(@conn, :create), fn f -> %>
- <%= label f, :email %>
- <%= email_input f, :email, required: true %>
-
-
- <%= submit "Resend confirmation instructions", class: "btn btn-primary" %>
-
-<% end %>
-
-
- <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
- <%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
-
diff --git a/lib/flix_web/templates/user_registration/new.html.heex b/lib/flix_web/templates/user_registration/new.html.heex
deleted file mode 100644
index 5950964..0000000
--- a/lib/flix_web/templates/user_registration/new.html.heex
+++ /dev/null
@@ -1,34 +0,0 @@
-Register
-
-<%= form_for @changeset, Routes.user_registration_path(@conn, :create), fn f -> %>
- <%= if @changeset.action do %>
-
-
Oops, something went wrong! Please check the errors below.
-
- <% end %>
-
- <%= label f, :name %>
- <%= text_input f, :name, required: true, autofocus: true %>
- <%= error_tag f, :name %>
-
- <%= label f, :email %>
- <%= email_input f, :email, required: true %>
- <%= error_tag f, :email %>
-
- <%= label f, :username %>
- <%= text_input f, :username %>
- <%= error_tag f, :username %>
-
- <%= label f, :password %>
- <%= password_input f, :password, required: true %>
- <%= error_tag f, :password %>
-
-
- <%= submit "Register", class: "btn btn-primary" %>
-
-<% end %>
-
-
- <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> |
- <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
-
diff --git a/lib/flix_web/templates/user_reset_password/edit.html.heex b/lib/flix_web/templates/user_reset_password/edit.html.heex
deleted file mode 100644
index 6e530db..0000000
--- a/lib/flix_web/templates/user_reset_password/edit.html.heex
+++ /dev/null
@@ -1,26 +0,0 @@
-Reset password
-
-<%= form_for @changeset, Routes.user_reset_password_path(@conn, :update, @token), fn f -> %>
- <%= if @changeset.action do %>
-
-
Oops, something went wrong! Please check the errors below.
-
- <% end %>
-
- <%= label f, :password, "New password" %>
- <%= password_input f, :password, required: true %>
- <%= error_tag f, :password %>
-
- <%= label f, :password_confirmation, "Confirm new password" %>
- <%= password_input f, :password_confirmation, required: true %>
- <%= error_tag f, :password_confirmation %>
-
-
- <%= submit "Reset password", class: "btn btn-primary" %>
-
-<% end %>
-
-
- <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
- <%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
-
diff --git a/lib/flix_web/templates/user_reset_password/new.html.heex b/lib/flix_web/templates/user_reset_password/new.html.heex
deleted file mode 100644
index 9c09d53..0000000
--- a/lib/flix_web/templates/user_reset_password/new.html.heex
+++ /dev/null
@@ -1,15 +0,0 @@
-Forgot your password?
-
-<%= form_for :user, Routes.user_reset_password_path(@conn, :create), fn f -> %>
- <%= label f, :email %>
- <%= email_input f, :email, required: true %>
-
-
- <%= submit "Send instructions to reset password", class: "btn btn-primary" %>
-
-<% end %>
-
-
- <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
- <%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
-
diff --git a/lib/flix_web/templates/user_session/new.html.heex b/lib/flix_web/templates/user_session/new.html.heex
deleted file mode 100644
index 043e6e4..0000000
--- a/lib/flix_web/templates/user_session/new.html.heex
+++ /dev/null
@@ -1,28 +0,0 @@
-Sign In
-
- No account yet? <%= link "Sign up!", to: Routes.user_registration_path(@conn, :new) %>
-
-
-<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user], fn f -> %>
- <%= if @error_message do %>
-
-
<%= @error_message %>
-
- <% end %>
-
- <%= label f, :email %>
- <%= email_input f, :email, required: true, autofocus: true %>
-
- <%= label f, :password %>
- <%= password_input f, :password, required: true %>
-
- <%= label f, :remember_me, "Keep me logged in for 60 days" %>
- <%= checkbox f, :remember_me %>
-
- <%= submit "Sign In", class: "btn btn-primary" %>
-<% end %>
-
-
- <%= link "Sign Up", to: Routes.user_registration_path(@conn, :new) %> |
- <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
-
diff --git a/lib/flix_web/templates/user_settings/edit.html.heex b/lib/flix_web/templates/user_settings/edit.html.heex
deleted file mode 100644
index de18bfb..0000000
--- a/lib/flix_web/templates/user_settings/edit.html.heex
+++ /dev/null
@@ -1,53 +0,0 @@
-Settings
-
-Change email
-
-<%= form_for @email_changeset, Routes.user_settings_path(@conn, :update), fn f -> %>
- <%= if @email_changeset.action do %>
-
-
Oops, something went wrong! Please check the errors below.
-
- <% end %>
-
- <%= hidden_input f, :action, name: "action", value: "update_email" %>
-
- <%= label f, :email %>
- <%= email_input f, :email, required: true %>
- <%= error_tag f, :email %>
-
- <%= label f, :current_password, for: "current_password_for_email" %>
- <%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email" %>
- <%= error_tag f, :current_password %>
-
-
- <%= submit "Change email", class: "btn btn-primary" %>
-
-<% end %>
-
-Change password
-
-<%= form_for @password_changeset, Routes.user_settings_path(@conn, :update), fn f -> %>
- <%= if @password_changeset.action do %>
-
-
Oops, something went wrong! Please check the errors below.
-
- <% end %>
-
- <%= hidden_input f, :action, name: "action", value: "update_password" %>
-
- <%= label f, :password, "New password" %>
- <%= password_input f, :password, required: true %>
- <%= error_tag f, :password %>
-
- <%= label f, :password_confirmation, "Confirm new password" %>
- <%= password_input f, :password_confirmation, required: true %>
- <%= error_tag f, :password_confirmation %>
-
- <%= label f, :current_password, for: "current_password_for_password" %>
- <%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password" %>
- <%= error_tag f, :current_password %>
-
-
- <%= submit "Change password" %>
-
-<% end %>
diff --git a/lib/flix_web/controllers/user_auth.ex b/lib/flix_web/user_auth.ex
similarity index 59%
rename from lib/flix_web/controllers/user_auth.ex
rename to lib/flix_web/user_auth.ex
index 68feaee..3b73c78 100644
--- a/lib/flix_web/controllers/user_auth.ex
+++ b/lib/flix_web/user_auth.ex
@@ -1,9 +1,10 @@
defmodule FlixWeb.UserAuth do
+ use FlixWeb, :verified_routes
+
import Plug.Conn
import Phoenix.Controller
alias Flix.Accounts
- alias FlixWeb.Router.Helpers, as: Routes
# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
@@ -30,10 +31,9 @@ defmodule FlixWeb.UserAuth do
conn
|> renew_session()
- |> put_session(:user_token, token)
- |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
+ |> put_token_in_session(token)
|> maybe_write_remember_me_cookie(token, params)
- |> redirect(to: user_return_to || "/fans/#{user.id}")
+ |> redirect(to: user_return_to || signed_in_path(conn))
end
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
@@ -72,7 +72,7 @@ defmodule FlixWeb.UserAuth do
"""
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
- user_token && Accounts.delete_session_token(user_token)
+ user_token && Accounts.delete_user_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do
FlixWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
@@ -81,7 +81,7 @@ defmodule FlixWeb.UserAuth do
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
- |> redirect(to: "/")
+ |> redirect(to: ~p"/")
end
@doc """
@@ -95,19 +95,91 @@ defmodule FlixWeb.UserAuth do
end
defp ensure_user_token(conn) do
- if user_token = get_session(conn, :user_token) do
- {user_token, conn}
+ if token = get_session(conn, :user_token) do
+ {token, conn}
else
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
- if user_token = conn.cookies[@remember_me_cookie] do
- {user_token, put_session(conn, :user_token, user_token)}
+ if token = conn.cookies[@remember_me_cookie] do
+ {token, put_token_in_session(conn, token)}
else
{nil, conn}
end
end
end
+ @doc """
+ Handles mounting and authenticating the current_user in LiveViews.
+
+ ## `on_mount` arguments
+
+ * `:mount_current_user` - Assigns current_user
+ to socket assigns based on user_token, or nil if
+ there's no user_token or no matching user.
+
+ * `:ensure_authenticated` - Authenticates the user from the session,
+ and assigns the current_user to socket assigns based
+ on user_token.
+ Redirects to login page if there's no logged user.
+
+ * `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
+ Redirects to signed_in_path if there's a logged user.
+
+ ## Examples
+
+ Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
+ the current_user:
+
+ defmodule FlixWeb.PageLive do
+ use FlixWeb, :live_view
+
+ on_mount {FlixWeb.UserAuth, :mount_current_user}
+ ...
+ end
+
+ Or use the `live_session` of your router to invoke the on_mount callback:
+
+ live_session :authenticated, on_mount: [{FlixWeb.UserAuth, :ensure_authenticated}] do
+ live "/profile", ProfileLive, :index
+ end
+ """
+ def on_mount(:mount_current_user, _params, session, socket) do
+ {:cont, mount_current_user(socket, session)}
+ end
+
+ def on_mount(:ensure_authenticated, _params, session, socket) do
+ socket = mount_current_user(socket, session)
+
+ if socket.assigns.current_user do
+ {:cont, socket}
+ else
+ socket =
+ socket
+ |> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
+ |> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
+
+ {:halt, socket}
+ end
+ end
+
+ def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
+ socket = mount_current_user(socket, session)
+
+ if socket.assigns.current_user do
+ {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
+ else
+ {:cont, socket}
+ end
+ end
+
+ defp mount_current_user(socket, session) do
+ Phoenix.Component.assign_new(socket, :current_user, fn ->
+ if user_token = session["user_token"] do
+ Accounts.get_user_by_session_token(user_token)
+ end
+ end)
+ end
+
@doc """
Used for routes that require the user to not be authenticated.
"""
@@ -134,16 +206,22 @@ defmodule FlixWeb.UserAuth do
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
- |> redirect(to: Routes.user_session_path(conn, :new))
+ |> redirect(to: ~p"/users/log_in")
|> halt()
end
end
+ defp put_token_in_session(conn, token) do
+ conn
+ |> put_session(:user_token, token)
+ |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
+ end
+
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end
defp maybe_store_return_to(conn), do: conn
- defp signed_in_path(_conn), do: "/"
+ defp signed_in_path(_conn), do: ~p"/"
end
diff --git a/mix.exs b/mix.exs
index ec68355..df79a65 100644
--- a/mix.exs
+++ b/mix.exs
@@ -7,7 +7,6 @@ defmodule Flix.MixProject do
version: "0.1.0",
elixir: "~> 1.16.2",
elixirc_paths: elixirc_paths(Mix.env()),
- compilers: [:gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
@@ -36,24 +35,32 @@ defmodule Flix.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
- {:phoenix, "~> 1.6.16"},
+ {:phoenix, "~> 1.7.11"},
{:phoenix_ecto, "~> 4.4.3"},
{:ecto_sql, "~> 3.10.1"},
{:postgrex, "~> 0.17.5"},
- {:phoenix_html, "~> 3.3.3"},
+ {:phoenix_html, "~> 4.1.1"},
{:phoenix_live_reload, "~> 1.3.3", only: :dev},
- {:phoenix_live_view, "~> 0.17.5"},
+ {:phoenix_live_view, "~> 0.20.2"},
{:floki, ">= 0.36.1", only: :test},
- {:phoenix_live_dashboard, "~> 0.5"},
- {:esbuild, "~> 0.7.1", runtime: Mix.env() == :dev},
- {:dart_sass, "~> 0.6.0", runtime: Mix.env() == :dev},
+ {:phoenix_live_dashboard, "~> 0.8.3"},
+ {:esbuild, "~> 0.8.1", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.2.2", runtime: Mix.env() == :dev},
- {:swoosh, "~> 1.11.6"},
+ {:heroicons,
+ github: "tailwindlabs/heroicons",
+ tag: "v2.1.1",
+ sparse: "optimized",
+ app: false,
+ compile: false,
+ depth: 1},
+ {:swoosh, "~> 1.16.3"},
+ {:finch, "~> 0.18.0"},
{:telemetry_metrics, "~> 0.6.2"},
{:telemetry_poller, "~> 1.0.0"},
- {:gettext, "~> 0.18.2"},
+ {:gettext, "~> 0.20.0"},
{:jason, "~> 1.3.0"},
- {:bandit, "~> 1.0.0"},
+ {:dns_cluster, "~> 0.1.1"},
+ {:bandit, "~> 1.4.1"},
{:absinthe_plug, "~> 1.5.8"},
{:cors_plug, "~> 3.0.3"},
{:bcrypt_elixir, "~> 2.0"},
@@ -67,8 +74,6 @@ defmodule Flix.MixProject do
{:hackney, "~> 1.18.2"},
{:sweet_xml, "~> 0.6"},
{:ex_parameterize, "~> 1.0"},
- {:bamboo, "~> 2.1.0"},
- {:bamboo_phoenix, "~> 1.0"},
{:credo, "~> 1.7.5", only: [:dev, :test], runtime: false},
{:mix_test_watch, "~> 1.1.2", only: [:dev, :test], runtime: false}
]
@@ -82,19 +87,15 @@ defmodule Flix.MixProject do
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
- setup: ["deps.get", "ecto.setup", "assets.build"],
+ setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
- "assets.build": [
- "esbuild default",
- "sass default",
- "tailwind default"
- ],
+ "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
+ "assets.build": ["tailwind flix", "esbuild flix"],
"assets.deploy": [
- "esbuild default --minify",
- "sass default --no-source-map --style=compressed",
- "tailwind default --minify",
+ "tailwind flix --minify",
+ "esbuild flix --minify",
"phx.digest"
]
]
diff --git a/mix.lock b/mix.lock
index 4927ff7..2a2105e 100644
--- a/mix.lock
+++ b/mix.lock
@@ -35,13 +35,13 @@
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
- "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
+ "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_test_watch": {:hex, :mix_test_watch, "1.1.2", "431bdccf20b110f1595fe2a0e3c6cffd96d8f706721def5d04d557bc0898c476", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "8ce79fc69a304eec81ab6c1a05de2eb026a8959f65fb47f933ce8eb56018ba35"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"number": {:hex, :number, "1.0.4", "3e6e6032a3c1d4c3760e77a42c580a57a15545dd993af380809da30fe51a032c", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "16f7516584ef2be812af4f33f2eaf3f9b9f6ed8892f45853eb93113f83721e42"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
- "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"},
+ "phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
"phoenix_html_simplified_helpers": {:hex, :phoenix_html_simplified_helpers, "2.1.0", "252c80df9a5bf8c8312b8fff57bc9735fcc07c599a2e825967d8b919b89a1eae", [:mix], [{:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, ">= 0.11.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:timex, "~> 3.2", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "5444ec916abfaa72e38fd57974d7ceb18a08433bbfc2c1094f0a5a5064c03165"},
@@ -54,7 +54,7 @@
"phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"},
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [: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", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
- "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
+ "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [: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", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
@@ -71,4 +71,5 @@
"waffle": {:hex, :waffle, "1.1.8", "9257275a10e7b58f968bf891d2433ecaeced801edc064b3c100dc510848b9760", [:mix], [{:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.1", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "30f1b00998ddb626d98f014bb7606173ff18610b1e89431ff84442dc1c94c6cd"},
"waffle_ecto": {:hex, :waffle_ecto, "0.0.12", "e5c17c49b071b903df71861c642093281123142dc4e9908c930db3e06795b040", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:waffle, "~> 1.0", [hex: :waffle, repo: "hexpm", optional: false]}], "hexpm", "585fe6371057066d2e8e3383ddd7a2437ff0668caf3f4cbf5a041e0de9837168"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
}
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index 7ff78f5..3d012a5 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -19,51 +19,20 @@ defmodule FlixWeb.ConnCase do
using do
quote do
+ # The default endpoint for testing
+ @endpoint FlixWeb.Endpoint
+
+ use FlixWeb, :verified_routes
+
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import FlixWeb.ConnCase
-
- alias FlixWeb.Router.Helpers, as: Routes
-
- # The default endpoint for testing
- @endpoint FlixWeb.Endpoint
end
end
setup tags do
- :ok = Ecto.Adapters.SQL.Sandbox.checkout(Flix.Repo)
-
- unless tags[:async] do
- Ecto.Adapters.SQL.Sandbox.mode(Flix.Repo, {:shared, self()})
- end
-
+ Flix.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
-
- @doc """
- Setup helper that registers and logs in users.
-
- setup :register_and_log_in_user
-
- It stores an updated connection and a registered user in the
- test context.
- """
- def register_and_log_in_user(%{conn: conn}) do
- user = Flix.AccountsFixtures.user_fixture()
- %{conn: log_in_user(conn, user), user: user}
- end
-
- @doc """
- Logs the given `user` into the `conn`.
-
- It returns an updated `conn`.
- """
- def log_in_user(conn, user) do
- token = Flix.Accounts.generate_user_session_token(user)
-
- conn
- |> Phoenix.ConnTest.init_test_session(%{})
- |> Plug.Conn.put_session(:user_token, token)
- end
end