diff --git a/config/prod.exs b/config/prod.exs index d5b270b..d2e23eb 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -11,8 +11,6 @@ import Config # before starting your production server. config :cannery, CanneryWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" -config :cannery, Cannery.Application, automigrate: true - # Do not print debug messages in production config :logger, level: :info diff --git a/config/runtime.exs b/config/runtime.exs index 5de081a..1bc702f 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -67,6 +67,9 @@ if config_env() == :prod do config :cannery, CanneryWeb.Endpoint, secret_key_base: secret_key_base + # Automatically apply migrations + config :cannery, Cannery.Application, automigrate: true + # Set up SMTP settings config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.SMTP, @@ -87,22 +90,4 @@ if config_env() == :prod do # # Then you can assemble a release by calling `mix release`. # See `mix help release` for more information. - - # ## Configuring the mailer - # - # In production you need to configure the mailer to use a different adapter. - # Also, you may need to configure the Swoosh API client of your choice if you - # are not using SMTP. Here is an example of the configuration: - # - # config :cannery, Cannery.Mailer, - # adapter: Swoosh.Adapters.Mailgun, - # api_key: System.get_env("MAILGUN_API_KEY"), - # domain: System.get_env("MAILGUN_DOMAIN") - # - # For this example you need include a HTTP client required by Swoosh API client. - # Swoosh supports Hackney and Finch out of the box: - # - # config :swoosh, :api_client, Swoosh.ApiClient.Hackney - # - # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. end diff --git a/lib/cannery/accounts.ex b/lib/cannery/accounts.ex index b584ee6..d4256e1 100644 --- a/lib/cannery/accounts.ex +++ b/lib/cannery/accounts.ex @@ -4,10 +4,10 @@ defmodule Cannery.Accounts do """ import Ecto.Query, warn: false - alias Cannery.Repo + alias Cannery.{Mailer, Repo} alias Cannery.Accounts.{User, UserToken} - alias Cannery.Mailer alias Ecto.{Changeset, Multi} + alias Oban.Job ## Database getters @@ -202,8 +202,7 @@ defmodule Cannery.Accounts do {:ok, %{to: ..., body: ...}} """ - @spec deliver_update_email_instructions(User.t(), String.t(), function) :: - {:ok, any()} | {:error, atom()} + @spec deliver_update_email_instructions(User.t(), String.t(), function) :: Job.t() def deliver_update_email_instructions(user, current_email, update_email_url_fun) when is_function(update_email_url_fun, 1) do {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}") @@ -319,8 +318,7 @@ defmodule Cannery.Accounts do {:error, :already_confirmed} """ - @spec deliver_user_confirmation_instructions(User.t(), function) :: - {:ok, any()} | {:error, atom()} + @spec deliver_user_confirmation_instructions(User.t(), function) :: Job.t() def deliver_user_confirmation_instructions(user, confirmation_url_fun) when is_function(confirmation_url_fun, 1) do if user.confirmed_at do @@ -367,8 +365,7 @@ defmodule Cannery.Accounts do {:ok, %{to: ..., body: ...}} """ - @spec deliver_user_reset_password_instructions(User.t(), function()) :: - {:ok, any()} | {:error, atom()} + @spec deliver_user_reset_password_instructions(User.t(), function()) :: Job.t() def deliver_user_reset_password_instructions(user, reset_password_url_fun) when is_function(reset_password_url_fun, 1) do {encoded_token, user_token} = UserToken.build_email_token(user, "reset_password") diff --git a/lib/cannery/accounts/email.ex b/lib/cannery/accounts/email.ex index 06136bc..299b90a 100644 --- a/lib/cannery/accounts/email.ex +++ b/lib/cannery/accounts/email.ex @@ -7,7 +7,7 @@ defmodule Cannery.Email do `lib/cannery_web/templates/layout/email.txt.heex` for text emails. """ - use Phoenix.Swoosh, view: Cannery.EmailView, layout: {Cannery.LayoutView, :email} + use Phoenix.Swoosh, view: CanneryWeb.EmailView, layout: {CanneryWeb.LayoutView, :email} import CanneryWeb.Gettext alias Cannery.Accounts.User alias CanneryWeb.EmailView @@ -19,33 +19,27 @@ defmodule Cannery.Email do @spec base_email(User.t(), String.t()) :: t() defp base_email(%User{email: email}, subject) do - new() - |> to(email) - |> from({ - Application.get_env(:cannery, Cannery.Mailer)[:email_name], - Application.get_env(:cannery, Cannery.Mailer)[:email_from] - }) - |> subject(subject) + from = Application.get_env(:cannery, Cannery.Mailer)[:email_from] || "noreply@localhost" + name = Application.get_env(:cannery, Cannery.Mailer)[:email_name] + new() |> to(email) |> from({name, from}) |> subject(subject) end - @spec welcome_email(User.t(), String.t()) :: t() - def welcome_email(user, url) do + @spec generate_email(String.t(), User.t(), attrs :: map()) :: t() + def generate_email("welcome", user, %{"url" => url}) do user |> base_email(dgettext("emails", "Confirm your %{name} account", name: "Cannery")) |> render_body("confirm_email.html", %{user: user, url: url}) |> text_body(EmailView.render("confirm_email.txt", %{user: user, url: url})) end - @spec reset_password_email(User.t(), String.t()) :: t() - def reset_password_email(user, url) do + def generate_email("reset_password", user, %{"url" => url}) do user |> base_email(dgettext("emails", "Reset your %{name} password", name: "Cannery")) |> render_body("reset_password.html", %{user: user, url: url}) |> text_body(EmailView.render("reset_password.txt", %{user: user, url: url})) end - @spec update_email(User.t(), String.t()) :: t() - def update_email(user, url) do + def generate_email("update_email", user, %{"url" => url}) do user |> base_email(dgettext("emails", "Update your %{name} email", name: "Cannery")) |> render_body("update_email.html", %{user: user, url: url}) diff --git a/lib/cannery/accounts/email_worker.ex b/lib/cannery/accounts/email_worker.ex index 7345dac..bee0b4b 100644 --- a/lib/cannery/accounts/email_worker.ex +++ b/lib/cannery/accounts/email_worker.ex @@ -4,10 +4,10 @@ defmodule Cannery.EmailWorker do """ use Oban.Worker, queue: :mailers - alias Cannery.Mailer + alias Cannery.{Accounts, Mailer, Email} @impl Oban.Worker - def perform(%Oban.Job{args: email}) do - email |> Mailer.deliver() + def perform(%Oban.Job{args: %{"email" => email, "user_id" => user_id, "attrs" => attrs}}) do + Email.generate_email(email, user_id |> Accounts.get_user!(), attrs) |> Mailer.deliver() end end diff --git a/lib/cannery/mailer.ex b/lib/cannery/mailer.ex index cb72e97..3b6d865 100644 --- a/lib/cannery/mailer.ex +++ b/lib/cannery/mailer.ex @@ -1,33 +1,40 @@ defmodule Cannery.Mailer do @moduledoc """ Mailer adapter for emails + + Since emails are loaded as Oban jobs, the `:attrs` map must be serializable to + json with Jason, which restricts the use of structs. """ use Swoosh.Mailer, otp_app: :cannery - alias Cannery.{Accounts.User, Email, EmailWorker} + alias Cannery.{Accounts.User, EmailWorker} alias Oban.Job @doc """ Deliver instructions to confirm account. """ - @spec deliver_confirmation_instructions(User.t(), String.t()) :: {:ok, Job.t()} - def deliver_confirmation_instructions(user, url) do - {:ok, Email.welcome_email(user, url) |> EmailWorker.new() |> Oban.insert!()} + @spec deliver_confirmation_instructions(User.t(), String.t()) :: Job.t() + def deliver_confirmation_instructions(%User{id: user_id}, url) do + %{email: :welcome, user_id: user_id, attrs: %{url: url}} + |> EmailWorker.new() + |> Oban.insert!() end @doc """ Deliver instructions to reset a user password. """ - @spec deliver_reset_password_instructions(User.t(), String.t()) :: {:ok, Job.t()} - def deliver_reset_password_instructions(user, url) do - {:ok, Email.reset_password_email(user, url) |> EmailWorker.new() |> Oban.insert!()} + @spec deliver_reset_password_instructions(User.t(), String.t()) :: Job.t() + def deliver_reset_password_instructions(%User{id: user_id}, url) do + %{email: :reset_password, user_id: user_id, attrs: %{url: url}} + |> EmailWorker.new() + |> Oban.insert!() end @doc """ Deliver instructions to update a user email. """ - @spec deliver_update_email_instructions(User.t(), String.t()) :: {:ok, Job.t()} - def deliver_update_email_instructions(user, url) do - {:ok, Email.update_email(user, url) |> EmailWorker.new() |> Oban.insert!()} + @spec deliver_update_email_instructions(User.t(), String.t()) :: Job.t() + def deliver_update_email_instructions(%User{id: user_id}, url) do + %{email: :update, user_id: user_id, attrs: %{url: url}} |> EmailWorker.new() |> Oban.insert!() end end diff --git a/lib/cannery_web/controllers/user_registration_controller.ex b/lib/cannery_web/controllers/user_registration_controller.ex index f93f398..65f87ea 100644 --- a/lib/cannery_web/controllers/user_registration_controller.ex +++ b/lib/cannery_web/controllers/user_registration_controller.ex @@ -62,11 +62,10 @@ defmodule CanneryWeb.UserRegistrationController do invite |> Invites.use_invite!() end - {:ok, _} = - Accounts.deliver_user_confirmation_instructions( - user, - &Routes.user_confirmation_url(conn, :confirm, &1) - ) + Accounts.deliver_user_confirmation_instructions( + user, + &Routes.user_confirmation_url(conn, :confirm, &1) + ) conn |> put_flash(:info, dgettext("prompts", "User created successfully.")) diff --git a/lib/cannery_web/views/layout_view.ex b/lib/cannery_web/views/layout_view.ex index bb47d2a..a725ee7 100644 --- a/lib/cannery_web/views/layout_view.ex +++ b/lib/cannery_web/views/layout_view.ex @@ -1,7 +1,7 @@ defmodule CanneryWeb.LayoutView do use CanneryWeb, :view alias Cannery.Accounts - alias CanneryWeb.HomeLive + alias CanneryWeb.{Endpoint, HomeLive} # Phoenix LiveDashboard is available only in development by default, # so we instruct Elixir to not warn if the dashboard route is missing. diff --git a/priv/gettext/actions.pot b/priv/gettext/actions.pot index 2b3edab..8d75449 100644 --- a/priv/gettext/actions.pot +++ b/priv/gettext/actions.pot @@ -13,10 +13,10 @@ msgstr "" #, elixir-format, ex-autogen #: lib/cannery_web/component/topbar.ex:96 #: lib/cannery_web/templates/layout/topbar.html.heex:36 -#: lib/cannery_web/templates/user_confirmation/new.html.heex:24 +#: lib/cannery_web/templates/user_confirmation/new.html.heex:26 #: lib/cannery_web/templates/user_registration/new.html.heex:39 -#: lib/cannery_web/templates/user_reset_password/edit.html.heex:39 -#: lib/cannery_web/templates/user_reset_password/new.html.heex:25 +#: lib/cannery_web/templates/user_reset_password/edit.html.heex:41 +#: lib/cannery_web/templates/user_reset_password/new.html.heex:27 #: lib/cannery_web/templates/user_session/new.html.heex:3 #: lib/cannery_web/templates/user_session/new.html.heex:35 msgid "Log in" @@ -25,12 +25,12 @@ msgstr "" #, elixir-format, ex-autogen #: lib/cannery_web/component/topbar.ex:89 #: lib/cannery_web/templates/layout/topbar.html.heex:28 -#: lib/cannery_web/templates/user_confirmation/new.html.heex:20 +#: lib/cannery_web/templates/user_confirmation/new.html.heex:21 #: lib/cannery_web/templates/user_registration/new.html.heex:3 #: lib/cannery_web/templates/user_registration/new.html.heex:34 -#: lib/cannery_web/templates/user_reset_password/edit.html.heex:35 -#: lib/cannery_web/templates/user_reset_password/new.html.heex:21 -#: lib/cannery_web/templates/user_session/new.html.heex:40 +#: lib/cannery_web/templates/user_reset_password/edit.html.heex:36 +#: lib/cannery_web/templates/user_reset_password/new.html.heex:22 +#: lib/cannery_web/templates/user_session/new.html.heex:41 msgid "Register" msgstr "" @@ -54,7 +54,7 @@ msgstr "" #, elixir-format, ex-autogen #: lib/cannery_web/templates/user_registration/new.html.heex:43 #: lib/cannery_web/templates/user_reset_password/new.html.heex:3 -#: lib/cannery_web/templates/user_session/new.html.heex:44 +#: lib/cannery_web/templates/user_session/new.html.heex:46 msgid "Forgot your password?" msgstr "" diff --git a/priv/gettext/emails.pot b/priv/gettext/emails.pot index 870f8dd..57abdba 100644 --- a/priv/gettext/emails.pot +++ b/priv/gettext/emails.pot @@ -21,17 +21,17 @@ msgid "This email was sent from %{name} at %{url}" msgstr "" #, elixir-format, ex-autogen -#: lib/cannery/accounts/email.ex:34 +#: lib/cannery/accounts/email.ex:30 msgid "Confirm your %{name} account" msgstr "" #, elixir-format, ex-autogen -#: lib/cannery/accounts/email.ex:42 +#: lib/cannery/accounts/email.ex:37 msgid "Reset your %{name} password" msgstr "" #, elixir-format, ex-autogen -#: lib/cannery/accounts/email.ex:50 +#: lib/cannery/accounts/email.ex:44 msgid "Update your %{name} email" msgstr "" diff --git a/priv/gettext/prompts.pot b/priv/gettext/prompts.pot index a7fc113..47baa54 100644 --- a/priv/gettext/prompts.pot +++ b/priv/gettext/prompts.pot @@ -67,7 +67,7 @@ msgid "User confirmed successfully." msgstr "" #, elixir-format, ex-autogen -#: lib/cannery_web/controllers/user_registration_controller.ex:72 +#: lib/cannery_web/controllers/user_registration_controller.ex:71 msgid "User created successfully." msgstr ""