diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md index 18036db..53662e2 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -1255,32 +1255,32 @@ mix deps.update phoenix mix hex.outdated ``` -### 3.11 Email: Swoosh +### 3.11 Email: Swoosh and Phoenix.Swoosh -**Mailer Configuration:** +**Mailer and from address:** + +- `Mv.Mailer` (Swoosh) and `Mv.Mailer.mail_from/0` return the configured sender `{name, email}`. +- Config: `config :mv, :mail_from, {"Mila", "noreply@example.com"}` in config.exs; override in runtime.exs from ENV for production. + +**Unified layout (transactional emails):** + +- All transactional emails (join confirmation, user confirmation, password reset) use the same layout: `MvWeb.EmailLayoutView` (layout) and `MvWeb.EmailsView` (body templates). +- Templates live under `lib/mv_web/templates/emails/` (bodies) and `lib/mv_web/templates/emails/layouts/` (layout). Use Gettext in templates for i18n. +- See `MvWeb.Emails.JoinConfirmationEmail`, `Mv.Accounts.User.Senders.SendNewUserConfirmationEmail`, `SendPasswordResetEmail` for the pattern; see `docs/email-layout-mockup.md` for layout structure. + +**Sending with layout:** ```elixir -defmodule Mv.Mailer do - use Swoosh.Mailer, otp_app: :mv -end -``` +use Phoenix.Swoosh, view: MvWeb.EmailsView, layout: {MvWeb.EmailLayoutView, "layout.html"} -**Sending Emails:** - -```elixir -defmodule Mv.Accounts.WelcomeEmail do - use Phoenix.Swoosh, template_root: "lib/mv_web/templates" - import Swoosh.Email - - def send(user) do - new() - |> to({user.name, user.email}) - |> from({"Mila", "noreply@mila.example.com"}) - |> subject("Welcome to Mila!") - |> render_body("welcome.html", %{user: user}) - |> Mv.Mailer.deliver() - end -end +new() +|> from(Mailer.mail_from()) +|> to(email_address) +|> subject(gettext("Subject")) +|> put_view(MvWeb.EmailsView) +|> put_layout({MvWeb.EmailLayoutView, "layout.html"}) +|> render_body("template_name.html", %{assigns}) +|> Mailer.deliver!() ``` ### 3.12 Internationalization: Gettext diff --git a/config/config.exs b/config/config.exs index d4de2c2..323f5cd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -89,6 +89,10 @@ config :mv, MvWeb.Endpoint, # at the `config/runtime.exs`. config :mv, Mv.Mailer, adapter: Swoosh.Adapters.Local +# Default mail "from" address for transactional emails (join confirmation, +# user confirmation, password reset). Override in config/runtime.exs from ENV. +config :mv, :mail_from, {"Mila", "noreply@example.com"} + # Configure esbuild (the version is required) config :esbuild, version: "0.17.11", diff --git a/config/runtime.exs b/config/runtime.exs index 93df5bb..d502cfa 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -219,6 +219,10 @@ if config_env() == :prod do # ## Configuring the mailer # + # Transactional emails (join confirmation, user confirmation, password reset) use + # the sender from config :mv, :mail_from. Override in production if needed: + # config :mv, :mail_from, {System.get_env("MAIL_FROM_NAME", "Mila"), System.get_env("MAIL_FROM_EMAIL", "noreply@example.com")} + # # 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: diff --git a/docs/development-progress-log.md b/docs/development-progress-log.md index 5d777ff..4e95ff4 100644 --- a/docs/development-progress-log.md +++ b/docs/development-progress-log.md @@ -800,6 +800,14 @@ end - Domain: `Mv.Membership.submit_join_request/2`, `Mv.Membership.confirm_join_request/2` (token hashing via `JoinRequest.hash_confirmation_token/1`, lookup, expiry check, idempotency for :submitted/:approved/:rejected). - Test file: `test/membership/join_request_test.exs` – all tests pass; policy test and expired-token test implemented. +**Subtask 2 – Submit and confirm flow (done):** +- **Unified email layout:** phoenix_swoosh with `MvWeb.EmailLayoutView` (layout) and `MvWeb.EmailsView` (body templates). All transactional emails (join confirmation, user confirmation, password reset) use the same layout. Config: `config :mv, :mail_from, {name, email}` (default `{"Mila", "noreply@example.com"}`); override in runtime.exs. +- **Join confirmation:** Domain wrapper `submit_join_request/2` generates token (or uses optional `:confirmation_token` in attrs for tests), creates JoinRequest via action `:submit`, then sends one email via `MvWeb.Emails.JoinConfirmationEmail`. Route `GET /confirm_join/:token` (JoinConfirmController) updates to `submitted`; idempotent; expired/invalid handled. +- **Senders migrated:** `SendNewUserConfirmationEmail`, `SendPasswordResetEmail` use layout + `Mv.Mailer.mail_from/0`. +- **Cleanup:** Mix task `mix join_requests.cleanup_expired` hard-deletes JoinRequests in `pending_confirmation` with expired `confirmation_token_expires_at` (authorize?: false). For cron/Oban. +- **Gettext:** New email strings in default domain; German translations in de/LC_MESSAGES/default.po. +- Tests: `join_request_test.exs`, `join_request_submit_email_test.exs`, `join_confirm_controller_test.exs` – all pass. + ### Test Data Management **Seed Data:** diff --git a/docs/email-layout-mockup.md b/docs/email-layout-mockup.md new file mode 100644 index 0000000..0c1d53b --- /dev/null +++ b/docs/email-layout-mockup.md @@ -0,0 +1,26 @@ +# Unified Email Layout – ASCII Mockup + +All transactional emails (join confirmation, user confirmation, password reset) use the same layout. + +``` ++------------------------------------------------------------------+ +| [Logo or app name – e.g. "Mila" or club name] | ++------------------------------------------------------------------+ +| | +| [Subject / heading line – e.g. "Confirm your email address"] | +| | +| [Body content – paragraph and CTA link] | +| e.g. "Please click the link below to confirm your request." | +| "Confirm my request" (button or link) | +| | +| [Optional: short note – e.g. "If you didn't request this, | +| you can ignore this email."] | +| | ++------------------------------------------------------------------+ +| [Footer – one line, e.g. "© 2025 Mila · Mitgliederverwaltung"] | ++------------------------------------------------------------------+ +``` + +- **Header:** Single line (app/club name), subtle. +- **Main:** Heading + body text + primary CTA (link/button). +- **Footer:** Single line, small text (copyright / product name). diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index d967b38..ffca356 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -30,6 +30,7 @@ defmodule Mv.Membership do import Ash.Expr alias Ash.Error.Query.NotFound, as: NotFoundError alias Mv.Membership.JoinRequest + alias MvWeb.Emails.JoinConfirmationEmail admin do show? true @@ -85,7 +86,7 @@ defmodule Mv.Membership do end resource Mv.Membership.JoinRequest do - define :submit_join_request, action: :submit + # submit_join_request/2 implemented as custom function below (create + send email) end end @@ -350,6 +351,47 @@ defmodule Mv.Membership do |> then(&Ash.read_one(query, &1)) end + @doc """ + Creates a join request (submit flow) and sends the confirmation email. + + Generates a confirmation token if not provided in attrs (e.g. for tests, pass + `:confirmation_token` to get a known token). On success, sends one email with + the confirm link to the request email. + + ## Options + - `:actor` - Must be nil for public submit (policy allows only unauthenticated). + + ## Returns + - `{:ok, request}` - Created JoinRequest in status pending_confirmation + - `{:error, error}` - Validation or authorization error + """ + def submit_join_request(attrs, opts \\ []) do + actor = Keyword.get(opts, :actor) + token = Map.get(attrs, :confirmation_token) || generate_confirmation_token() + + attrs_with_token = + attrs |> Map.drop([:confirmation_token]) |> Map.put(:confirmation_token, token) + + case Ash.create(JoinRequest, attrs_with_token, + action: :submit, + actor: actor, + domain: __MODULE__ + ) do + {:ok, request} -> + JoinConfirmationEmail.send(request.email, token) + {:ok, request} + + error -> + error + end + end + + defp generate_confirmation_token do + 32 + |> :crypto.strong_rand_bytes() + |> Base.url_encode64(padding: false) + end + @doc """ Confirms a join request by token (public confirmation link). diff --git a/lib/mix/tasks/join_requests.cleanup_expired.ex b/lib/mix/tasks/join_requests.cleanup_expired.ex new file mode 100644 index 0000000..bc9ea2a --- /dev/null +++ b/lib/mix/tasks/join_requests.cleanup_expired.ex @@ -0,0 +1,56 @@ +defmodule Mix.Tasks.JoinRequests.CleanupExpired do + @moduledoc """ + Hard-deletes JoinRequests in status `pending_confirmation` whose confirmation link has expired. + + Retention: records with `confirmation_token_expires_at` older than now are deleted. + Intended for cron or Oban (e.g. every hour). See docs/onboarding-join-concept.md. + + ## Usage + + mix join_requests.cleanup_expired + + ## Examples + + $ mix join_requests.cleanup_expired + Deleted 3 expired join request(s). + """ + use Mix.Task + + require Ash.Query + + alias Mv.Membership.JoinRequest + + @shortdoc "Deletes join requests in pending_confirmation with expired confirmation token" + + @impl Mix.Task + def run(_args) do + Mix.Task.run("app.start") + + now = DateTime.utc_now() + + query = + JoinRequest + |> Ash.Query.filter(status == :pending_confirmation) + |> Ash.Query.filter(confirmation_token_expires_at < ^now) + + # Bypass authorization: cleanup is a system maintenance task (cron/Oban). + case Ash.read(query, domain: Mv.Membership, authorize?: false) do + {:ok, requests} -> + count = delete_expired_requests(requests) + Mix.shell().info("Deleted #{count} expired join request(s).") + count + + {:error, error} -> + Mix.raise("Failed to list expired join requests: #{inspect(error)}") + end + end + + defp delete_expired_requests(requests) do + Enum.reduce_while(requests, 0, fn request, acc -> + case Ash.destroy(request, domain: Mv.Membership, authorize?: false) do + :ok -> {:cont, acc + 1} + {:error, _} -> {:halt, acc} + end + end) + end +end diff --git a/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex b/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex index 2135465..7c5fa0c 100644 --- a/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex +++ b/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex @@ -1,12 +1,20 @@ defmodule Mv.Accounts.User.Senders.SendNewUserConfirmationEmail do @moduledoc """ Sends an email for a new user to confirm their email address. + + Uses the unified email layout (MvWeb.EmailsView + EmailLayoutView) and + central mail from config (Mv.Mailer.mail_from/0). """ use AshAuthentication.Sender - use MvWeb, :verified_routes + use Phoenix.Swoosh, + view: MvWeb.EmailsView, + layout: {MvWeb.EmailLayoutView, "layout.html"} + + use MvWeb, :verified_routes import Swoosh.Email + use Gettext, backend: MvWeb.Gettext, otp_app: :mv alias Mv.Mailer @@ -26,21 +34,16 @@ defmodule Mv.Accounts.User.Senders.SendNewUserConfirmationEmail do """ @impl true def send(user, token, _) do + confirm_url = url(~p"/confirm_new_user/#{token}") + subject = gettext("Confirm your email address") + new() - # Replace with email from env - |> from({"noreply", "noreply@example.com"}) + |> from(Mailer.mail_from()) |> to(to_string(user.email)) - |> subject("Confirm your email address") - |> html_body(body(token: token)) + |> subject(subject) + |> put_view(MvWeb.EmailsView) + |> put_layout({MvWeb.EmailLayoutView, "layout.html"}) + |> render_body("user_confirmation.html", %{confirm_url: confirm_url, subject: subject}) |> Mailer.deliver!() end - - defp body(params) do - url = url(~p"/confirm_new_user/#{params[:token]}") - - """ -

Click this link to confirm your email:

-

#{url}

- """ - end end diff --git a/lib/mv/accounts/user/senders/send_password_reset_email.ex b/lib/mv/accounts/user/senders/send_password_reset_email.ex index bcf4e75..a4bb489 100644 --- a/lib/mv/accounts/user/senders/send_password_reset_email.ex +++ b/lib/mv/accounts/user/senders/send_password_reset_email.ex @@ -1,12 +1,20 @@ defmodule Mv.Accounts.User.Senders.SendPasswordResetEmail do @moduledoc """ - Sends a password reset email + Sends a password reset email. + + Uses the unified email layout (MvWeb.EmailsView + EmailLayoutView) and + central mail from config (Mv.Mailer.mail_from/0). """ use AshAuthentication.Sender - use MvWeb, :verified_routes + use Phoenix.Swoosh, + view: MvWeb.EmailsView, + layout: {MvWeb.EmailLayoutView, "layout.html"} + + use MvWeb, :verified_routes import Swoosh.Email + use Gettext, backend: MvWeb.Gettext, otp_app: :mv alias Mv.Mailer @@ -26,21 +34,16 @@ defmodule Mv.Accounts.User.Senders.SendPasswordResetEmail do """ @impl true def send(user, token, _) do + reset_url = url(~p"/password-reset/#{token}") + subject = gettext("Reset your password") + new() - # Replace with email from env - |> from({"noreply", "noreply@example.com"}) + |> from(Mailer.mail_from()) |> to(to_string(user.email)) - |> subject("Reset your password") - |> html_body(body(token: token)) + |> subject(subject) + |> put_view(MvWeb.EmailsView) + |> put_layout({MvWeb.EmailLayoutView, "layout.html"}) + |> render_body("password_reset.html", %{reset_url: reset_url, subject: subject}) |> Mailer.deliver!() end - - defp body(params) do - url = url(~p"/password-reset/#{params[:token]}") - - """ -

Click this link to reset your password:

-

#{url}

- """ - end end diff --git a/lib/mv/mailer.ex b/lib/mv/mailer.ex index 43d5c39..3d83636 100644 --- a/lib/mv/mailer.ex +++ b/lib/mv/mailer.ex @@ -1,3 +1,19 @@ defmodule Mv.Mailer do + @moduledoc """ + Swoosh mailer for transactional emails. + + Use `mail_from/0` for the configured sender address (join confirmation, + user confirmation, password reset). + """ use Swoosh.Mailer, otp_app: :mv + + @doc """ + Returns the configured "from" address for transactional emails. + + Configure in config.exs or runtime.exs as `config :mv, :mail_from, {name, email}`. + Default: `{"Mila", "noreply@example.com"}`. + """ + def mail_from do + Application.get_env(:mv, :mail_from, {"Mila", "noreply@example.com"}) + end end diff --git a/lib/mv_web/emails/email_layout_view.ex b/lib/mv_web/emails/email_layout_view.ex new file mode 100644 index 0000000..a0cf03f --- /dev/null +++ b/lib/mv_web/emails/email_layout_view.ex @@ -0,0 +1,12 @@ +defmodule MvWeb.EmailLayoutView do + @moduledoc """ + Layout view for transactional emails (join confirmation, user confirmation, password reset). + + Renders a single layout template that wraps all email body content. + See docs/email-layout-mockup.md for the layout structure. + """ + use Phoenix.View, + root: "lib/mv_web", + path: "templates/emails/layouts", + namespace: MvWeb +end diff --git a/lib/mv_web/emails/emails_view.ex b/lib/mv_web/emails/emails_view.ex new file mode 100644 index 0000000..87bb29d --- /dev/null +++ b/lib/mv_web/emails/emails_view.ex @@ -0,0 +1,13 @@ +defmodule MvWeb.EmailsView do + @moduledoc """ + View for transactional email body templates. + + Templates are rendered inside EmailLayoutView layout when sent via Phoenix.Swoosh. + """ + use Phoenix.View, + root: "lib/mv_web", + path: "templates/emails", + namespace: MvWeb + + use Gettext, backend: MvWeb.Gettext, otp_app: :mv +end diff --git a/lib/mv_web/emails/join_confirmation_email.ex b/lib/mv_web/emails/join_confirmation_email.ex new file mode 100644 index 0000000..f72ec45 --- /dev/null +++ b/lib/mv_web/emails/join_confirmation_email.ex @@ -0,0 +1,33 @@ +defmodule MvWeb.Emails.JoinConfirmationEmail do + @moduledoc """ + Sends the join request confirmation email (double opt-in) using the unified email layout. + """ + use Phoenix.Swoosh, + view: MvWeb.EmailsView, + layout: {MvWeb.EmailLayoutView, :layout} + + use MvWeb, :verified_routes + import Swoosh.Email + use Gettext, backend: MvWeb.Gettext, otp_app: :mv + + alias Mv.Mailer + + @doc """ + Sends the join confirmation email to the given address with the confirmation link. + + Called from the domain after a JoinRequest is created (submit flow). + """ + def send(email_address, token) when is_binary(email_address) and is_binary(token) do + confirm_url = url(~p"/confirm_join/#{token}") + subject = gettext("Confirm your membership request") + + new() + |> from(Mailer.mail_from()) + |> to(email_address) + |> subject(subject) + |> put_view(MvWeb.EmailsView) + |> put_layout({MvWeb.EmailLayoutView, "layout.html"}) + |> render_body("join_confirmation.html", %{confirm_url: confirm_url, subject: subject}) + |> Mailer.deliver!() + end +end diff --git a/lib/mv_web/templates/emails/join_confirmation.html.heex b/lib/mv_web/templates/emails/join_confirmation.html.heex new file mode 100644 index 0000000..b8344eb --- /dev/null +++ b/lib/mv_web/templates/emails/join_confirmation.html.heex @@ -0,0 +1,18 @@ +
+

+ {gettext( + "We have received your membership request. To complete it, please click the link below." + )} +

+

+ + {gettext("Confirm my request")} + +

+

+ {gettext("If you did not submit this request, you can ignore this email.")} +

+
diff --git a/lib/mv_web/templates/emails/layouts/layout.html.heex b/lib/mv_web/templates/emails/layouts/layout.html.heex new file mode 100644 index 0000000..63bc5c7 --- /dev/null +++ b/lib/mv_web/templates/emails/layouts/layout.html.heex @@ -0,0 +1,33 @@ + + + + + + {assigns[:subject] || "Mila"} + + + + + + + + + + + + +
+
Mila
+
+ {@inner_content} +
+ © {DateTime.utc_now().year} Mila · Mitgliederverwaltung +
+ + diff --git a/lib/mv_web/templates/emails/password_reset.html.heex b/lib/mv_web/templates/emails/password_reset.html.heex new file mode 100644 index 0000000..8dece57 --- /dev/null +++ b/lib/mv_web/templates/emails/password_reset.html.heex @@ -0,0 +1,18 @@ +
+

+ {gettext("You requested a password reset. Click the link below to set a new password.")} +

+

+ + {gettext("Reset password")} + +

+

+ {gettext( + "If you did not request this, you can ignore this email. Your password will remain unchanged." + )} +

+
diff --git a/lib/mv_web/templates/emails/user_confirmation.html.heex b/lib/mv_web/templates/emails/user_confirmation.html.heex new file mode 100644 index 0000000..2ba9702 --- /dev/null +++ b/lib/mv_web/templates/emails/user_confirmation.html.heex @@ -0,0 +1,16 @@ +
+

+ {gettext("Please confirm your email address by clicking the link below.")} +

+

+ + {gettext("Confirm my email")} + +

+

+ {gettext("If you did not create an account, you can ignore this email.")} +

+
diff --git a/mix.exs b/mix.exs index 19c598c..0d1d4f1 100644 --- a/mix.exs +++ b/mix.exs @@ -65,6 +65,7 @@ defmodule Mv.MixProject do app: false, compile: false, depth: 1}, + {:phoenix_swoosh, "~> 1.0"}, {:swoosh, "~> 1.16"}, {:req, "~> 0.5"}, {:telemetry_metrics, "~> 1.0"}, diff --git a/mix.lock b/mix.lock index 7e96c47..849dfd5 100644 --- a/mix.lock +++ b/mix.lock @@ -65,6 +65,7 @@ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.26", "306af67d6557cc01f880107cc459f1fa0acbaab60bc8c027a368ba16b3544473", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, 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.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0ec34b24c69aa70c4f25a8901effe3462bee6c8ca80a9a4a7685215e3a0ac34e"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, + "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [: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 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [: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", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"}, diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index ec26b39..ac959b7 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -3249,3 +3249,78 @@ msgstr[1] "%{count} Filter aktiv" #, elixir-autogen, elixir-format msgid "without %{name}" msgstr "ohne %{name}" + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Confirm my email" +msgstr "E-Mail bestätigen" + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Confirm my request" +msgstr "Anfrage bestätigen" + +#: lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex +#, elixir-autogen, elixir-format +msgid "Confirm your email address" +msgstr "E-Mail-Adresse bestätigen" + +#: lib/mv_web/emails/join_confirmation_email.ex +#, elixir-autogen, elixir-format +msgid "Confirm your membership request" +msgstr "Mitgliedschaftsanfrage bestätigen" + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not create an account, you can ignore this email." +msgstr "Wenn Sie kein Konto angelegt haben, können Sie diese E-Mail ignorieren." + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not request this, you can ignore this email. Your password will remain unchanged." +msgstr "Wenn Sie das nicht angefordert haben, können Sie diese E-Mail ignorieren. Ihr Passwort bleibt unverändert." + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not submit this request, you can ignore this email." +msgstr "Wenn Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren." + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "Invalid or expired link." +msgstr "Ungültiger oder abgelaufener Link." + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Please confirm your email address by clicking the link below." +msgstr "Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Link klicken." + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "Reset password" +msgstr "Passwort zurücksetzen" + +#: lib/mv/accounts/user/senders/send_password_reset_email.ex +#, elixir-autogen, elixir-format +msgid "Reset your password" +msgstr "Passwort zurücksetzen" + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "Thank you, we have received your request." +msgstr "Vielen Dank, wir haben Ihre Anfrage erhalten." + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "This link has expired. Please submit the form again." +msgstr "Dieser Link ist abgelaufen. Bitte senden Sie das Formular erneut ab." + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "We have received your membership request. To complete it, please click the link below." +msgstr "Wir haben Ihre Mitgliedschaftsanfrage erhalten. Bitte klicken Sie zur Bestätigung auf den folgenden Link." + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "You requested a password reset. Click the link below to set a new password." +msgstr "Sie haben die Zurücksetzung Ihres Passworts angefordert. Klicken Sie auf den folgenden Link, um ein neues Passwort zu setzen." diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index d5efdd8..5748796 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -3249,3 +3249,78 @@ msgstr[1] "" #, elixir-autogen, elixir-format msgid "without %{name}" msgstr "" + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Confirm my email" +msgstr "" + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Confirm my request" +msgstr "" + +#: lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex +#, elixir-autogen, elixir-format +msgid "Confirm your email address" +msgstr "" + +#: lib/mv_web/emails/join_confirmation_email.ex +#, elixir-autogen, elixir-format +msgid "Confirm your membership request" +msgstr "" + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not create an account, you can ignore this email." +msgstr "" + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not request this, you can ignore this email. Your password will remain unchanged." +msgstr "" + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not submit this request, you can ignore this email." +msgstr "" + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "Invalid or expired link." +msgstr "" + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Please confirm your email address by clicking the link below." +msgstr "" + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "Reset password" +msgstr "" + +#: lib/mv/accounts/user/senders/send_password_reset_email.ex +#, elixir-autogen, elixir-format +msgid "Reset your password" +msgstr "" + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "Thank you, we have received your request." +msgstr "" + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "This link has expired. Please submit the form again." +msgstr "" + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "We have received your membership request. To complete it, please click the link below." +msgstr "" + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "You requested a password reset. Click the link below to set a new password." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 9a76cc8..f8932f5 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -3249,3 +3249,78 @@ msgstr[1] "%{count} filters active" #, elixir-autogen, elixir-format msgid "without %{name}" msgstr "without %{name}" + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Confirm my email" +msgstr "" + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Confirm my request" +msgstr "" + +#: lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex +#, elixir-autogen, elixir-format +msgid "Confirm your email address" +msgstr "" + +#: lib/mv_web/emails/join_confirmation_email.ex +#, elixir-autogen, elixir-format +msgid "Confirm your membership request" +msgstr "" + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not create an account, you can ignore this email." +msgstr "" + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not request this, you can ignore this email. Your password will remain unchanged." +msgstr "" + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "If you did not submit this request, you can ignore this email." +msgstr "" + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "Invalid or expired link." +msgstr "" + +#: lib/mv_web/templates/emails/user_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "Please confirm your email address by clicking the link below." +msgstr "" + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "Reset password" +msgstr "" + +#: lib/mv/accounts/user/senders/send_password_reset_email.ex +#, elixir-autogen, elixir-format +msgid "Reset your password" +msgstr "" + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "Thank you, we have received your request." +msgstr "" + +#: lib/mv_web/controllers/join_confirm_controller.ex +#, elixir-autogen, elixir-format +msgid "This link has expired. Please submit the form again." +msgstr "" + +#: lib/mv_web/templates/emails/join_confirmation.html.heex +#, elixir-autogen, elixir-format +msgid "We have received your membership request. To complete it, please click the link below." +msgstr "" + +#: lib/mv_web/templates/emails/password_reset.html.heex +#, elixir-autogen, elixir-format +msgid "You requested a password reset. Click the link below to set a new password." +msgstr "" diff --git a/test/membership/join_request_submit_email_test.exs b/test/membership/join_request_submit_email_test.exs index 87c989a..5f648d4 100644 --- a/test/membership/join_request_submit_email_test.exs +++ b/test/membership/join_request_submit_email_test.exs @@ -26,6 +26,7 @@ defmodule Mv.Membership.JoinRequestSubmitEmailTest do assert_email_sent(fn email_sent -> to_addresses = Enum.map(email_sent.to, &elem(&1, 1)) + to_string(email) in to_addresses and (email_sent.html_body =~ "/confirm_join/" or email_sent.text_body =~ "/confirm_join/") end) diff --git a/test/mv_web/controllers/join_confirm_controller_test.exs b/test/mv_web/controllers/join_confirm_controller_test.exs index a8e4334..d1e9117 100644 --- a/test/mv_web/controllers/join_confirm_controller_test.exs +++ b/test/mv_web/controllers/join_confirm_controller_test.exs @@ -87,7 +87,6 @@ defmodule MvWeb.JoinConfirmControllerTest do conn = get(conn, "/confirm_join/public-test-token") assert conn.status == 200 - refute redirected_to(conn) =~ "/sign-in" end end end