feat: add join confirmation and mail templating
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-03-09 18:15:12 +01:00
parent 3672ef0d03
commit 6385fbc831
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
24 changed files with 585 additions and 53 deletions

View file

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

View file

@ -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",

View file

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

View file

@ -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:**

View file

@ -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).

View file

@ -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).

View file

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

View file

@ -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]}")
"""
<p>Click this link to confirm your email:</p>
<p><a href="#{url}">#{url}</a></p>
"""
end
end

View file

@ -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]}")
"""
<p>Click this link to reset your password:</p>
<p><a href="#{url}">#{url}</a></p>
"""
end
end

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,18 @@
<div style="color: #111827;">
<p style="margin: 0 0 16px; font-size: 16px; line-height: 1.5;">
{gettext(
"We have received your membership request. To complete it, please click the link below."
)}
</p>
<p style="margin: 0 0 24px;">
<a
href={@confirm_url}
style="display: inline-block; padding: 12px 24px; background-color: #2563eb; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500;"
>
{gettext("Confirm my request")}
</a>
</p>
<p style="margin: 0; font-size: 14px; color: #6b7280;">
{gettext("If you did not submit this request, you can ignore this email.")}
</p>
</div>

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{assigns[:subject] || "Mila"}</title>
</head>
<body style="margin: 0; padding: 0; font-family: system-ui, -apple-system, sans-serif; background-color: #f3f4f6;">
<table
role="presentation"
width="100%"
cellspacing="0"
cellpadding="0"
style="max-width: 600px; margin: 0 auto;"
>
<tr>
<td style="padding: 24px 16px 16px;">
<div style="font-size: 18px; font-weight: 600; color: #111827;">Mila</div>
</td>
</tr>
<tr>
<td style="padding: 16px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
{@inner_content}
</td>
</tr>
<tr>
<td style="padding: 16px 24px; font-size: 12px; color: #6b7280;">
© {DateTime.utc_now().year} Mila · Mitgliederverwaltung
</td>
</tr>
</table>
</body>
</html>

View file

@ -0,0 +1,18 @@
<div style="color: #111827;">
<p style="margin: 0 0 16px; font-size: 16px; line-height: 1.5;">
{gettext("You requested a password reset. Click the link below to set a new password.")}
</p>
<p style="margin: 0 0 24px;">
<a
href={@reset_url}
style="display: inline-block; padding: 12px 24px; background-color: #2563eb; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500;"
>
{gettext("Reset password")}
</a>
</p>
<p style="margin: 0; font-size: 14px; color: #6b7280;">
{gettext(
"If you did not request this, you can ignore this email. Your password will remain unchanged."
)}
</p>
</div>

View file

@ -0,0 +1,16 @@
<div style="color: #111827;">
<p style="margin: 0 0 16px; font-size: 16px; line-height: 1.5;">
{gettext("Please confirm your email address by clicking the link below.")}
</p>
<p style="margin: 0 0 24px;">
<a
href={@confirm_url}
style="display: inline-block; padding: 12px 24px; background-color: #2563eb; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500;"
>
{gettext("Confirm my email")}
</a>
</p>
<p style="margin: 0; font-size: 14px; color: #6b7280;">
{gettext("If you did not create an account, you can ignore this email.")}
</p>
</div>

View file

@ -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"},

View file

@ -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"},

View file

@ -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."

View file

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

View file

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

View file

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

View file

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