From a39c3da35122b760f9427cb43e784a5e8248ab96 Mon Sep 17 00:00:00 2001 From: shibao Date: Tue, 8 Feb 2022 19:59:23 -0500 Subject: [PATCH] add swoosh and oban --- CONTRIBUTING.md | 8 ++ README.md | 8 ++ config/config.exs | 6 ++ config/runtime.exs | 15 +++- config/test.exs | 3 + docker-compose.yml | 9 +- lib/cannery/accounts.ex | 9 +- lib/cannery/accounts/email.ex | 54 ++++++++++++ lib/cannery/accounts/email_worker.ex | 9 ++ lib/cannery/accounts/user_notifier.ex | 77 ---------------- lib/cannery/application.ex | 8 +- lib/cannery/mailer.ex | 26 ++++++ .../templates/email/confirm_email.html.eex | 12 +++ .../templates/email/confirm_email.txt.eex | 12 +++ .../templates/email/reset_password.html.eex | 10 +++ .../templates/email/reset_password.txt.eex | 10 +++ .../templates/email/update_email.html.eex | 10 +++ .../templates/email/update_email.txt.eex | 10 +++ .../templates/layout/email.html.heex | 16 ++++ .../templates/layout/email.txt.eex | 13 +++ lib/cannery_web/views/email_view.ex | 8 ++ mix.exs | 2 + mix.lock | 2 + priv/gettext/default.pot | 11 +++ priv/gettext/emails.pot | 88 +++++++++++++++++++ .../20220209003648_add_oban_jobs_table.exs | 13 +++ 26 files changed, 365 insertions(+), 84 deletions(-) create mode 100644 lib/cannery/accounts/email.ex create mode 100644 lib/cannery/accounts/email_worker.ex delete mode 100644 lib/cannery/accounts/user_notifier.ex create mode 100644 lib/cannery_web/templates/email/confirm_email.html.eex create mode 100644 lib/cannery_web/templates/email/confirm_email.txt.eex create mode 100644 lib/cannery_web/templates/email/reset_password.html.eex create mode 100644 lib/cannery_web/templates/email/reset_password.txt.eex create mode 100644 lib/cannery_web/templates/email/update_email.html.eex create mode 100644 lib/cannery_web/templates/email/update_email.txt.eex create mode 100644 lib/cannery_web/templates/layout/email.html.heex create mode 100644 lib/cannery_web/templates/layout/email.txt.eex create mode 100644 lib/cannery_web/views/email_view.ex create mode 100644 priv/gettext/default.pot create mode 100644 priv/gettext/emails.pot create mode 100644 priv/repo/migrations/20220209003648_add_oban_jobs_table.exs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d067b78..9635718 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,3 +107,11 @@ In `prod` mode (or in the Docker container), Cannery will listen for the same en - `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated with `docker run -it shibaobun/cannery mix phx.gen.secret` and set for server to start. +- `SMTP_HOST`: The url for your SMTP email provider. Must be set +- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`. +- `SMTP_USERNAME`: The username for your SMTP relay. Must be set! +- `SMTP_PASSWORD`: The password for your SMTP relay. Must be set! +- `SMTP_SSL`: Set to `true` to enable SSL for emails. Defaults to `false`. +- `EMAIL_FROM`: Sets the sender email in sent emails. Defaults to + `no-reply@HOST` where `HOST` was previously defined. +- `EMAIL_NAME`: Sets the sender name in sent emails. Defaults to "Cannery". diff --git a/README.md b/README.md index 8ebd1ae..b574f3f 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,14 @@ You can use the following environment variables to configure Cannery in - `REGISTRATION`: Controls if user sign-up should be invite only or set to public. Set to `public` to enable public registration. Defaults to `invite`. - `LOCALE`: Sets a custom locale. Defaults to `en_US`. +- `SMTP_HOST`: The url for your SMTP email provider. Must be set +- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`. +- `SMTP_USERNAME`: The username for your SMTP relay. Must be set! +- `SMTP_PASSWORD`: The password for your SMTP relay. Must be set! +- `SMTP_SSL`: Set to `true` to enable SSL for emails. Defaults to `false`. +- `EMAIL_FROM`: Sets the sender email in sent emails. Defaults to + `no-reply@HOST` where `HOST` was previously defined. +- `EMAIL_NAME`: Sets the sender name in sent emails. Defaults to "Cannery". # Contribution diff --git a/config/config.exs b/config/config.exs index 3083f46..bada0d6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -43,6 +43,12 @@ config :swoosh, :api_client, false # Gettext config :gettext, :default_locale, "en_US" +# Configure Oban +config :cannery, Oban, + repo: Cannery.Repo, + plugins: [Oban.Plugins.Pruner], + queues: [default: 10, mailers: 20] + # Configure esbuild (the version is required) # config :esbuild, # version: "0.14.0", diff --git a/config/runtime.exs b/config/runtime.exs index 03e51c8..5de081a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -26,7 +26,9 @@ database_url = "ecto://postgres:postgres@cannery-db/cannery" end -host = System.get_env("HOST") || "localhost" +host = + System.get_env("HOST") || + raise "No hostname set! Must be the domain and tld like `cannery.bubbletea.dev`." interface = if config_env() in [:dev, :test], @@ -65,6 +67,17 @@ if config_env() == :prod do config :cannery, CanneryWeb.Endpoint, secret_key_base: secret_key_base + # Set up SMTP settings + config :cannery, Cannery.Mailer, + adapter: Swoosh.Adapters.SMTP, + relay: System.get_env("SMTP_HOST") || raise("No SMTP_HOST set!"), + port: System.get_env("SMTP_PORT") || 587, + username: System.get_env("SMTP_USERNAME") || raise("No SMTP_USERNAME set!"), + password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"), + ssl: System.get_env("SMTP_SSL") == "true", + email_from: System.get_env("EMAIL_FROM") || "no-reply@#{System.get_env("HOST")}", + email_name: System.get_env("EMAIL_NAME") || "Cannery" + # ## Using releases # # If you are doing OTP releases, you need to instruct Phoenix diff --git a/config/test.exs b/config/test.exs index 24fae83..718ffc7 100644 --- a/config/test.exs +++ b/config/test.exs @@ -27,3 +27,6 @@ config :logger, level: :warn # Initialize plugs at runtime for faster test compilation config :phoenix, :plug_init_mode, :runtime + +# Disable Oban +config :cannery, Oban, queues: false, plugins: false diff --git a/docker-compose.yml b/docker-compose.yml index f97dace..dffd4b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,15 @@ services: - DATABASE_URL="ecto://postgres:postgres@cannery-db/cannery" # Use `docker run -it shibaobun/cannery mix phx.gen.secret` to generate a secret key base - SECRET_KEY_BASE="change-me-this-is-really-important-seriously-change-it" - # uncomment to enable sign ups, watch for spam! + # uncomment to enable public sign ups, not recommended # - REGISTRATION="public" + # - SMTP_HOST="cannery.example.tld" # must be set! + # - SMTP_PORT="587" # optional + # - SMTP_USERNAME="username" + # - SMTP_PASSWORD="password" + # - SMTP_SSL="false" # optional + # - EMAIL_FROM="no-reply@cannery.example.tld" # optional + # - EMAIL_NAME="Cannery" # optional expose: - "4000" depends_on: diff --git a/lib/cannery/accounts.ex b/lib/cannery/accounts.ex index a1b1f06..ba31af4 100644 --- a/lib/cannery/accounts.ex +++ b/lib/cannery/accounts.ex @@ -5,7 +5,8 @@ defmodule Cannery.Accounts do import Ecto.Query, warn: false alias Cannery.Repo - alias Cannery.Accounts.{User, UserNotifier, UserToken} + alias Cannery.Accounts.{User, UserToken} + alias Cannery.Mailer alias Ecto.{Changeset, Multi} ## Database getters @@ -208,7 +209,7 @@ defmodule Cannery.Accounts do {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}") Repo.insert!(user_token) - UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token)) + Mailer.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token)) end @doc """ @@ -319,7 +320,7 @@ defmodule Cannery.Accounts do else {encoded_token, user_token} = UserToken.build_email_token(user, "confirm") Repo.insert!(user_token) - UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token)) + Mailer.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token)) end end @@ -364,7 +365,7 @@ defmodule Cannery.Accounts do when is_function(reset_password_url_fun, 1) do {encoded_token, user_token} = UserToken.build_email_token(user, "reset_password") Repo.insert!(user_token) - UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token)) + Mailer.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token)) end @doc """ diff --git a/lib/cannery/accounts/email.ex b/lib/cannery/accounts/email.ex new file mode 100644 index 0000000..06136bc --- /dev/null +++ b/lib/cannery/accounts/email.ex @@ -0,0 +1,54 @@ +defmodule Cannery.Email do + @moduledoc """ + Emails that can be sent using Swoosh. + + You can find the base email templates at + `lib/cannery_web/templates/layout/email.html.heex` for html emails and + `lib/cannery_web/templates/layout/email.txt.heex` for text emails. + """ + + use Phoenix.Swoosh, view: Cannery.EmailView, layout: {Cannery.LayoutView, :email} + import CanneryWeb.Gettext + alias Cannery.Accounts.User + alias CanneryWeb.EmailView + + @typedoc """ + Represents an HTML and text body email that can be sent + """ + @type t() :: Swoosh.Email.t() + + @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) + end + + @spec welcome_email(User.t(), String.t()) :: t() + def welcome_email(user, 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 + 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 + user + |> base_email(dgettext("emails", "Update your %{name} email", name: "Cannery")) + |> render_body("update_email.html", %{user: user, url: url}) + |> text_body(EmailView.render("update_email.txt", %{user: user, url: url})) + end +end diff --git a/lib/cannery/accounts/email_worker.ex b/lib/cannery/accounts/email_worker.ex new file mode 100644 index 0000000..5198c71 --- /dev/null +++ b/lib/cannery/accounts/email_worker.ex @@ -0,0 +1,9 @@ +defmodule Cannery.EmailWorker do + use Oban.Worker, queue: :mailers + alias Cannery.Mailer + + @impl Oban.Worker + def perform(%Oban.Job{args: email}) do + email |> Mailer.deliver() + end +end diff --git a/lib/cannery/accounts/user_notifier.ex b/lib/cannery/accounts/user_notifier.ex deleted file mode 100644 index f4c5360..0000000 --- a/lib/cannery/accounts/user_notifier.ex +++ /dev/null @@ -1,77 +0,0 @@ -defmodule Cannery.Accounts.UserNotifier do - @moduledoc """ - Contains all user emails and notifications - """ - - # For simplicity, this module simply logs messages to the terminal. - # You should replace it by a proper email or notification tool, such as: - # - # * Swoosh - https://hexdocs.pm/swoosh - # * Bamboo - https://hexdocs.pm/bamboo - # - defp deliver(to, body) do - require Logger - Logger.debug(body) - {:ok, %{to: to, body: body}} - end - - @doc """ - Deliver instructions to confirm account. - """ - def deliver_confirmation_instructions(user, url) do - deliver(user.email, """ - - ============================== - - 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 - deliver(user.email, """ - - ============================== - - 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 - deliver(user.email, """ - - ============================== - - 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/cannery/application.ex b/lib/cannery/application.ex index 9ad1c7d..82c2d11 100644 --- a/lib/cannery/application.ex +++ b/lib/cannery/application.ex @@ -15,7 +15,9 @@ defmodule Cannery.Application do # Start the PubSub system {Phoenix.PubSub, name: Cannery.PubSub}, # Start the Endpoint (http/https) - CanneryWeb.Endpoint + CanneryWeb.Endpoint, + # Add Oban + {Oban, oban_config()} # Start a worker by calling: Cannery.Worker.start_link(arg) # {Cannery.Worker, arg} ] @@ -39,4 +41,8 @@ defmodule Cannery.Application do CanneryWeb.Endpoint.config_change(changed, removed) :ok end + + defp oban_config() do + Application.fetch_env!(:cannery, Oban) + end end diff --git a/lib/cannery/mailer.ex b/lib/cannery/mailer.ex index ee9a784..cb72e97 100644 --- a/lib/cannery/mailer.ex +++ b/lib/cannery/mailer.ex @@ -4,4 +4,30 @@ defmodule Cannery.Mailer do """ use Swoosh.Mailer, otp_app: :cannery + alias Cannery.{Accounts.User, Email, 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!()} + 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!()} + 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!()} + end end diff --git a/lib/cannery_web/templates/email/confirm_email.html.eex b/lib/cannery_web/templates/email/confirm_email.html.eex new file mode 100644 index 0000000..18adab5 --- /dev/null +++ b/lib/cannery_web/templates/email/confirm_email.html.eex @@ -0,0 +1,12 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "Welcome to %{name}!", name: "Cannery") %> + +<%= dgettext("emails", "You can confirm your account by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't create an account at %{url}, please ignore this.", + url: Routes.live_url(Endpoint, HomeLive)) %> diff --git a/lib/cannery_web/templates/email/confirm_email.txt.eex b/lib/cannery_web/templates/email/confirm_email.txt.eex new file mode 100644 index 0000000..9f8ded8 --- /dev/null +++ b/lib/cannery_web/templates/email/confirm_email.txt.eex @@ -0,0 +1,12 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "Welcome to %{name}%!", name: "Cannery") %> + +<%= dgettext("emails", "You can confirm your account by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't create an account at %{url}, please ignore this.", + url: Routes.live_url(Endpoint, HomeLive)) %> diff --git a/lib/cannery_web/templates/email/reset_password.html.eex b/lib/cannery_web/templates/email/reset_password.html.eex new file mode 100644 index 0000000..cf9efb3 --- /dev/null +++ b/lib/cannery_web/templates/email/reset_password.html.eex @@ -0,0 +1,10 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "You can reset your password by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't request this change from %{url}, please ignore this.", + url: Routes.live_url(Endpoint, HomeLive)) %> diff --git a/lib/cannery_web/templates/email/reset_password.txt.eex b/lib/cannery_web/templates/email/reset_password.txt.eex new file mode 100644 index 0000000..cf9efb3 --- /dev/null +++ b/lib/cannery_web/templates/email/reset_password.txt.eex @@ -0,0 +1,10 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "You can reset your password by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't request this change from %{url}, please ignore this.", + url: Routes.live_url(Endpoint, HomeLive)) %> diff --git a/lib/cannery_web/templates/email/update_email.html.eex b/lib/cannery_web/templates/email/update_email.html.eex new file mode 100644 index 0000000..c23da11 --- /dev/null +++ b/lib/cannery_web/templates/email/update_email.html.eex @@ -0,0 +1,10 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "You can change your email by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't request this change from %{url}, please ignore this.", + url: Routes.live_url(Endpoint, HomeLive)) %> diff --git a/lib/cannery_web/templates/email/update_email.txt.eex b/lib/cannery_web/templates/email/update_email.txt.eex new file mode 100644 index 0000000..c23da11 --- /dev/null +++ b/lib/cannery_web/templates/email/update_email.txt.eex @@ -0,0 +1,10 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "You can change your email by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't request this change from %{url}, please ignore this.", + url: Routes.live_url(Endpoint, HomeLive)) %> diff --git a/lib/cannery_web/templates/layout/email.html.heex b/lib/cannery_web/templates/layout/email.html.heex new file mode 100644 index 0000000..bc32423 --- /dev/null +++ b/lib/cannery_web/templates/layout/email.html.heex @@ -0,0 +1,16 @@ + + + + <%= @email.subject %> + + + + <%= @inner_content %> + +
+ + + <%= dgettext("emails", "This email was sent from %{name}", name: "Cannery") %> + + + diff --git a/lib/cannery_web/templates/layout/email.txt.eex b/lib/cannery_web/templates/layout/email.txt.eex new file mode 100644 index 0000000..b1d02a1 --- /dev/null +++ b/lib/cannery_web/templates/layout/email.txt.eex @@ -0,0 +1,13 @@ +<%= @email.subject %> + + +==================== + +<%= @inner_content %> + +===================== + +<%= dgettext("emails", + "This email was sent from %{name} at %{url}", + name: "Cannery", + url: Routes.live_url(Endpoint, HomeLive)) %> diff --git a/lib/cannery_web/views/email_view.ex b/lib/cannery_web/views/email_view.ex new file mode 100644 index 0000000..60056ae --- /dev/null +++ b/lib/cannery_web/views/email_view.ex @@ -0,0 +1,8 @@ +defmodule CanneryWeb.EmailView do + @moduledoc """ + A view for email-related helper functions + """ + alias CanneryWeb.{Endpoint, HomeLive} + + use CanneryWeb, :view +end diff --git a/mix.exs b/mix.exs index 3a4995e..baef7da 100644 --- a/mix.exs +++ b/mix.exs @@ -46,6 +46,8 @@ defmodule Cannery.MixProject do {:phoenix_live_dashboard, "~> 0.6"}, # {:esbuild, "~> 0.3", runtime: Mix.env() == :dev}, {:swoosh, "~> 1.3"}, + {:phoenix_swoosh, "~> 1.0"}, + {:oban, "~> 2.10"}, {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, {:gettext, "~> 0.18"}, diff --git a/mix.lock b/mix.lock index 336b8ac..94d2473 100644 --- a/mix.lock +++ b/mix.lock @@ -24,6 +24,7 @@ "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, + "oban": {:hex, :oban, "2.10.1", "202a90f2aed0130b7d750bdbfea8090c8321bce255bade10fd3699733565add0", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "161cdd01194147cd6a3efdb1d6c3d9689309991412f799c1e242c18912e307c3"}, "phoenix": {:hex, :phoenix, "1.6.6", "281c8ce8dccc9f60607346b72cdfc597c3dde134dd9df28dff08282f0b751754", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.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", "807bd646e64cd9dc83db016199715faba72758e6db1de0707eef0a2da4924364"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, @@ -31,6 +32,7 @@ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.6", "3665f3ec426ac8d681cd7753ad4c85d2d247094dc4dc6add80dd6e3026045389", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "62f06d4bbfc4dc5595070bc338119ab08e8e67a011e2923f9366419622149b9c"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, + "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.0.0", "ac7cb8971935439c9757b266ac28cf90a940f506833fb548b1795cb5645c36a0", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "49341c80657f0ef3a65b033d7a47e743463fbe5e950f4c72d83e3a60042c2ad3"}, "phoenix_view": {:hex, :phoenix_view, "1.1.1", "11a945509d1270ef42bb19ca8f609aab378fc02543c2d4bdb8d243f1ca88f9b5", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1f8148a9163ca4b40065ef177c9c56c0df174cf3596a27995a1a1657f0c949e7"}, "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, "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"}, diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot new file mode 100644 index 0000000..bb4a02c --- /dev/null +++ b/priv/gettext/default.pot @@ -0,0 +1,11 @@ +## This file is a PO Template file. +## +## "msgid"s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run "mix gettext.extract" to bring this file up to +## date. Leave "msgstr"s empty as changing them here has no +## effect: edit them in PO (.po) files instead. +msgid "" +msgstr "" diff --git a/priv/gettext/emails.pot b/priv/gettext/emails.pot new file mode 100644 index 0000000..870f8dd --- /dev/null +++ b/priv/gettext/emails.pot @@ -0,0 +1,88 @@ +## This file is a PO Template file. +## +## "msgid"s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run "mix gettext.extract" to bring this file up to +## date. Leave "msgstr"s empty as changing them here has no +## effect: edit them in PO (.po) files instead. +msgid "" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/layout/email.html.heex:13 +msgid "This email was sent from %{name}" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/layout/email.txt.eex:10 +msgid "This email was sent from %{name} at %{url}" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery/accounts/email.ex:34 +msgid "Confirm your %{name} account" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery/accounts/email.ex:42 +msgid "Reset your %{name} password" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery/accounts/email.ex:50 +msgid "Update your %{name} email" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/email/confirm_email.html.eex:2 +#: lib/cannery_web/templates/email/confirm_email.txt.eex:2 +#: lib/cannery_web/templates/email/reset_password.html.eex:2 +#: lib/cannery_web/templates/email/reset_password.txt.eex:2 +#: lib/cannery_web/templates/email/update_email.html.eex:2 +#: lib/cannery_web/templates/email/update_email.txt.eex:2 +msgid "Hi %{email}," +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/email/confirm_email.html.eex:10 +#: lib/cannery_web/templates/email/confirm_email.txt.eex:10 +msgid "If you didn't create an account at %{url}, please ignore this." +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/email/reset_password.html.eex:8 +#: lib/cannery_web/templates/email/reset_password.txt.eex:8 +#: lib/cannery_web/templates/email/update_email.html.eex:8 +#: lib/cannery_web/templates/email/update_email.txt.eex:8 +msgid "If you didn't request this change from %{url}, please ignore this." +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/email/confirm_email.html.eex:4 +msgid "Welcome to %{name}!" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/email/confirm_email.txt.eex:4 +msgid "Welcome to %{name}%!" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/email/update_email.html.eex:4 +#: lib/cannery_web/templates/email/update_email.txt.eex:4 +msgid "You can change your email by visiting the URL below:" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/email/confirm_email.html.eex:6 +#: lib/cannery_web/templates/email/confirm_email.txt.eex:6 +msgid "You can confirm your account by visiting the URL below:" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/templates/email/reset_password.html.eex:4 +#: lib/cannery_web/templates/email/reset_password.txt.eex:4 +msgid "You can reset your password by visiting the URL below:" +msgstr "" diff --git a/priv/repo/migrations/20220209003648_add_oban_jobs_table.exs b/priv/repo/migrations/20220209003648_add_oban_jobs_table.exs new file mode 100644 index 0000000..af54101 --- /dev/null +++ b/priv/repo/migrations/20220209003648_add_oban_jobs_table.exs @@ -0,0 +1,13 @@ +defmodule Cannery.Repo.Migrations.AddObanJobsTable do + use Ecto.Migration + + def up do + Oban.Migrations.up() + end + + # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if + # necessary, regardless of which version we've migrated `up` to. + def down do + Oban.Migrations.down(version: 1) + end +end