From e614872c781a847919261df8672cdcc12336486e Mon Sep 17 00:00:00 2001 From: Conrad Taylor Date: Sat, 30 Mar 2024 21:11:26 -0700 Subject: [PATCH] Initial commit for Phoenix 1.7 migration. --- .formatter.exs | 7 +- LICENSE.md | 2 +- README.md | 2 +- config/config.exs | 42 +- config/dev.exs | 44 +- config/prod.exs | 21 +- config/runtime.exs | 64 +- config/test.exs | 11 +- lib/flix/accounts/user_notifier.ex | 63 +- lib/flix/accounts/user_token.ex | 80 ++- lib/flix/application.ex | 16 +- lib/flix/mailer.ex | 2 +- lib/flix_web.ex | 107 +-- lib/flix_web/components/core_components.ex | 675 ++++++++++++++++++ lib/flix_web/components/layouts.ex | 5 + lib/flix_web/components/layouts/app.html.heex | 32 + .../components/layouts/root.html.heex | 17 + .../confirmation_instructions.html.heex | 0 .../confirmation_instructions.text.heex | 0 .../reset_password_instructions.html.heex | 0 .../reset_password_instructions.text.heex | 0 .../update_email_instructions.html.heex | 0 .../update_email_instructions.text.heex | 0 lib/flix_web/controllers/fan_html.ex | 5 + .../fan_html}/index.html.heex | 0 .../fan_html}/show.html.heex | 0 lib/flix_web/controllers/favorite_html.ex | 5 + lib/flix_web/controllers/genre_html.ex | 5 + .../genre_html}/edit.html.heex | 0 .../genre_html}/form.html.heex | 0 .../genre_html}/index.html.heex | 0 .../genre_html}/new.html.heex | 0 .../genre_html}/show.html.heex | 0 lib/flix_web/controllers/movie_html.ex | 5 + .../movie_html}/edit.html.heex | 0 .../movie_html}/form.html.heex | 0 .../movie_html}/index.html.heex | 0 .../movie_html}/new.html.heex | 0 .../movie_html}/show.html.heex | 0 lib/flix_web/controllers/page_controller.ex | 6 +- lib/flix_web/controllers/page_html.ex | 5 + .../controllers/page_html/home.html.heex | 222 ++++++ lib/flix_web/controllers/review_html.ex | 5 + .../review_html}/edit.html.heex | 0 .../review_html}/form.html.heex | 0 .../review_html}/index.html.heex | 0 .../review_html}/new.html.heex | 0 .../shared_html}/_errors.html.heex | 0 .../shared_html}/_stars.html.heex | 0 .../user_confirmation_controller.ex | 25 +- .../controllers/user_confirmation_html.ex | 5 + .../user_confirmation_html/edit.html.heex | 14 + .../user_confirmation_html/new.html.heex | 21 + .../user_registration_controller.ex | 8 +- .../controllers/user_registration_html.ex | 5 + .../user_registration_html/new.html.heex | 26 + .../user_reset_password_controller.ex | 19 +- .../controllers/user_reset_password_html.ex | 5 + .../user_reset_password_html/edit.html.heex | 30 + .../user_reset_password_html/new.html.heex | 21 + .../controllers/user_session_controller.ex | 9 +- lib/flix_web/controllers/user_session_html.ex | 5 + .../user_session_html/new.html.heex | 30 + .../controllers/user_settings_controller.ex | 40 +- .../controllers/user_settings_html.ex | 5 + .../user_settings_html/edit.html.heex | 63 ++ lib/flix_web/endpoint.ex | 28 +- lib/flix_web/router.ex | 41 +- lib/flix_web/telemetry.ex | 47 +- lib/flix_web/templates/page/index.html.heex | 38 - .../templates/user_confirmation/new.html.heex | 15 - .../templates/user_registration/new.html.heex | 34 - .../user_reset_password/edit.html.heex | 26 - .../user_reset_password/new.html.heex | 15 - .../templates/user_session/new.html.heex | 28 - .../templates/user_settings/edit.html.heex | 53 -- lib/flix_web/{controllers => }/user_auth.ex | 102 ++- mix.exs | 43 +- mix.lock | 7 +- test/support/conn_case.ex | 43 +- 80 files changed, 1747 insertions(+), 552 deletions(-) create mode 100644 lib/flix_web/components/core_components.ex create mode 100644 lib/flix_web/components/layouts.ex create mode 100644 lib/flix_web/components/layouts/app.html.heex create mode 100644 lib/flix_web/components/layouts/root.html.heex rename lib/flix_web/{templates/email => controllers/email_html}/confirmation_instructions.html.heex (100%) rename lib/flix_web/{templates/email => controllers/email_html}/confirmation_instructions.text.heex (100%) rename lib/flix_web/{templates/email => controllers/email_html}/reset_password_instructions.html.heex (100%) rename lib/flix_web/{templates/email => controllers/email_html}/reset_password_instructions.text.heex (100%) rename lib/flix_web/{templates/email => controllers/email_html}/update_email_instructions.html.heex (100%) rename lib/flix_web/{templates/email => controllers/email_html}/update_email_instructions.text.heex (100%) create mode 100644 lib/flix_web/controllers/fan_html.ex rename lib/flix_web/{templates/fan => controllers/fan_html}/index.html.heex (100%) rename lib/flix_web/{templates/fan => controllers/fan_html}/show.html.heex (100%) create mode 100644 lib/flix_web/controllers/favorite_html.ex create mode 100644 lib/flix_web/controllers/genre_html.ex rename lib/flix_web/{templates/genre => controllers/genre_html}/edit.html.heex (100%) rename lib/flix_web/{templates/genre => controllers/genre_html}/form.html.heex (100%) rename lib/flix_web/{templates/genre => controllers/genre_html}/index.html.heex (100%) rename lib/flix_web/{templates/genre => controllers/genre_html}/new.html.heex (100%) rename lib/flix_web/{templates/genre => controllers/genre_html}/show.html.heex (100%) create mode 100644 lib/flix_web/controllers/movie_html.ex rename lib/flix_web/{templates/movie => controllers/movie_html}/edit.html.heex (100%) rename lib/flix_web/{templates/movie => controllers/movie_html}/form.html.heex (100%) rename lib/flix_web/{templates/movie => controllers/movie_html}/index.html.heex (100%) rename lib/flix_web/{templates/movie => controllers/movie_html}/new.html.heex (100%) rename lib/flix_web/{templates/movie => controllers/movie_html}/show.html.heex (100%) create mode 100644 lib/flix_web/controllers/page_html.ex create mode 100644 lib/flix_web/controllers/page_html/home.html.heex create mode 100644 lib/flix_web/controllers/review_html.ex rename lib/flix_web/{templates/review => controllers/review_html}/edit.html.heex (100%) rename lib/flix_web/{templates/review => controllers/review_html}/form.html.heex (100%) rename lib/flix_web/{templates/review => controllers/review_html}/index.html.heex (100%) rename lib/flix_web/{templates/review => controllers/review_html}/new.html.heex (100%) rename lib/flix_web/{templates/shared => controllers/shared_html}/_errors.html.heex (100%) rename lib/flix_web/{templates/shared => controllers/shared_html}/_stars.html.heex (100%) create mode 100644 lib/flix_web/controllers/user_confirmation_html.ex create mode 100644 lib/flix_web/controllers/user_confirmation_html/edit.html.heex create mode 100644 lib/flix_web/controllers/user_confirmation_html/new.html.heex create mode 100644 lib/flix_web/controllers/user_registration_html.ex create mode 100644 lib/flix_web/controllers/user_registration_html/new.html.heex create mode 100644 lib/flix_web/controllers/user_reset_password_html.ex create mode 100644 lib/flix_web/controllers/user_reset_password_html/edit.html.heex create mode 100644 lib/flix_web/controllers/user_reset_password_html/new.html.heex create mode 100644 lib/flix_web/controllers/user_session_html.ex create mode 100644 lib/flix_web/controllers/user_session_html/new.html.heex create mode 100644 lib/flix_web/controllers/user_settings_html.ex create mode 100644 lib/flix_web/controllers/user_settings_html/edit.html.heex delete mode 100644 lib/flix_web/templates/page/index.html.heex delete mode 100644 lib/flix_web/templates/user_confirmation/new.html.heex delete mode 100644 lib/flix_web/templates/user_registration/new.html.heex delete mode 100644 lib/flix_web/templates/user_reset_password/edit.html.heex delete mode 100644 lib/flix_web/templates/user_reset_password/new.html.heex delete mode 100644 lib/flix_web/templates/user_session/new.html.heex delete mode 100644 lib/flix_web/templates/user_settings/edit.html.heex rename lib/flix_web/{controllers => }/user_auth.ex (59%) 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""" + + """ + end + + def input(%{type: "select"} = assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.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""" + + """ + 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""" +
+
+

+ <%= render_slot(@inner_block) %> +

+

+ <%= render_slot(@subtitle) %> +

+
+
<%= render_slot(@actions) %>
+
+ """ + 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

-
- -
- - -
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