feat: add join confirmation and mail templating
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
3672ef0d03
commit
6385fbc831
24 changed files with 585 additions and 53 deletions
|
|
@ -1255,32 +1255,32 @@ mix deps.update phoenix
|
||||||
mix hex.outdated
|
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
|
```elixir
|
||||||
defmodule Mv.Mailer do
|
use Phoenix.Swoosh, view: MvWeb.EmailsView, layout: {MvWeb.EmailLayoutView, "layout.html"}
|
||||||
use Swoosh.Mailer, otp_app: :mv
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sending Emails:**
|
new()
|
||||||
|
|> from(Mailer.mail_from())
|
||||||
```elixir
|
|> to(email_address)
|
||||||
defmodule Mv.Accounts.WelcomeEmail do
|
|> subject(gettext("Subject"))
|
||||||
use Phoenix.Swoosh, template_root: "lib/mv_web/templates"
|
|> put_view(MvWeb.EmailsView)
|
||||||
import Swoosh.Email
|
|> put_layout({MvWeb.EmailLayoutView, "layout.html"})
|
||||||
|
|> render_body("template_name.html", %{assigns})
|
||||||
def send(user) do
|
|> Mailer.deliver!()
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.12 Internationalization: Gettext
|
### 3.12 Internationalization: Gettext
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,10 @@ config :mv, MvWeb.Endpoint,
|
||||||
# at the `config/runtime.exs`.
|
# at the `config/runtime.exs`.
|
||||||
config :mv, Mv.Mailer, adapter: Swoosh.Adapters.Local
|
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)
|
# Configure esbuild (the version is required)
|
||||||
config :esbuild,
|
config :esbuild,
|
||||||
version: "0.17.11",
|
version: "0.17.11",
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,10 @@ if config_env() == :prod do
|
||||||
|
|
||||||
# ## Configuring the mailer
|
# ## 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.
|
# 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
|
# 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:
|
# are not using SMTP. Here is an example of the configuration:
|
||||||
|
|
|
||||||
|
|
@ -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).
|
- 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.
|
- 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
|
### Test Data Management
|
||||||
|
|
||||||
**Seed Data:**
|
**Seed Data:**
|
||||||
|
|
|
||||||
26
docs/email-layout-mockup.md
Normal file
26
docs/email-layout-mockup.md
Normal 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).
|
||||||
|
|
@ -30,6 +30,7 @@ defmodule Mv.Membership do
|
||||||
import Ash.Expr
|
import Ash.Expr
|
||||||
alias Ash.Error.Query.NotFound, as: NotFoundError
|
alias Ash.Error.Query.NotFound, as: NotFoundError
|
||||||
alias Mv.Membership.JoinRequest
|
alias Mv.Membership.JoinRequest
|
||||||
|
alias MvWeb.Emails.JoinConfirmationEmail
|
||||||
|
|
||||||
admin do
|
admin do
|
||||||
show? true
|
show? true
|
||||||
|
|
@ -85,7 +86,7 @@ defmodule Mv.Membership do
|
||||||
end
|
end
|
||||||
|
|
||||||
resource Mv.Membership.JoinRequest do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -350,6 +351,47 @@ defmodule Mv.Membership do
|
||||||
|> then(&Ash.read_one(query, &1))
|
|> then(&Ash.read_one(query, &1))
|
||||||
end
|
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 """
|
@doc """
|
||||||
Confirms a join request by token (public confirmation link).
|
Confirms a join request by token (public confirmation link).
|
||||||
|
|
||||||
|
|
|
||||||
56
lib/mix/tasks/join_requests.cleanup_expired.ex
Normal file
56
lib/mix/tasks/join_requests.cleanup_expired.ex
Normal 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
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
defmodule Mv.Accounts.User.Senders.SendNewUserConfirmationEmail do
|
defmodule Mv.Accounts.User.Senders.SendNewUserConfirmationEmail do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Sends an email for a new user to confirm their email address.
|
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 AshAuthentication.Sender
|
||||||
use MvWeb, :verified_routes
|
|
||||||
|
|
||||||
|
use Phoenix.Swoosh,
|
||||||
|
view: MvWeb.EmailsView,
|
||||||
|
layout: {MvWeb.EmailLayoutView, "layout.html"}
|
||||||
|
|
||||||
|
use MvWeb, :verified_routes
|
||||||
import Swoosh.Email
|
import Swoosh.Email
|
||||||
|
use Gettext, backend: MvWeb.Gettext, otp_app: :mv
|
||||||
|
|
||||||
alias Mv.Mailer
|
alias Mv.Mailer
|
||||||
|
|
||||||
|
|
@ -26,21 +34,16 @@ defmodule Mv.Accounts.User.Senders.SendNewUserConfirmationEmail do
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def send(user, token, _) do
|
def send(user, token, _) do
|
||||||
|
confirm_url = url(~p"/confirm_new_user/#{token}")
|
||||||
|
subject = gettext("Confirm your email address")
|
||||||
|
|
||||||
new()
|
new()
|
||||||
# Replace with email from env
|
|> from(Mailer.mail_from())
|
||||||
|> from({"noreply", "noreply@example.com"})
|
|
||||||
|> to(to_string(user.email))
|
|> to(to_string(user.email))
|
||||||
|> subject("Confirm your email address")
|
|> subject(subject)
|
||||||
|> html_body(body(token: token))
|
|> put_view(MvWeb.EmailsView)
|
||||||
|
|> put_layout({MvWeb.EmailLayoutView, "layout.html"})
|
||||||
|
|> render_body("user_confirmation.html", %{confirm_url: confirm_url, subject: subject})
|
||||||
|> Mailer.deliver!()
|
|> Mailer.deliver!()
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
defmodule Mv.Accounts.User.Senders.SendPasswordResetEmail do
|
defmodule Mv.Accounts.User.Senders.SendPasswordResetEmail do
|
||||||
@moduledoc """
|
@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 AshAuthentication.Sender
|
||||||
use MvWeb, :verified_routes
|
|
||||||
|
|
||||||
|
use Phoenix.Swoosh,
|
||||||
|
view: MvWeb.EmailsView,
|
||||||
|
layout: {MvWeb.EmailLayoutView, "layout.html"}
|
||||||
|
|
||||||
|
use MvWeb, :verified_routes
|
||||||
import Swoosh.Email
|
import Swoosh.Email
|
||||||
|
use Gettext, backend: MvWeb.Gettext, otp_app: :mv
|
||||||
|
|
||||||
alias Mv.Mailer
|
alias Mv.Mailer
|
||||||
|
|
||||||
|
|
@ -26,21 +34,16 @@ defmodule Mv.Accounts.User.Senders.SendPasswordResetEmail do
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def send(user, token, _) do
|
def send(user, token, _) do
|
||||||
|
reset_url = url(~p"/password-reset/#{token}")
|
||||||
|
subject = gettext("Reset your password")
|
||||||
|
|
||||||
new()
|
new()
|
||||||
# Replace with email from env
|
|> from(Mailer.mail_from())
|
||||||
|> from({"noreply", "noreply@example.com"})
|
|
||||||
|> to(to_string(user.email))
|
|> to(to_string(user.email))
|
||||||
|> subject("Reset your password")
|
|> subject(subject)
|
||||||
|> html_body(body(token: token))
|
|> put_view(MvWeb.EmailsView)
|
||||||
|
|> put_layout({MvWeb.EmailLayoutView, "layout.html"})
|
||||||
|
|> render_body("password_reset.html", %{reset_url: reset_url, subject: subject})
|
||||||
|> Mailer.deliver!()
|
|> Mailer.deliver!()
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
defmodule Mv.Mailer do
|
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
|
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
|
end
|
||||||
|
|
|
||||||
12
lib/mv_web/emails/email_layout_view.ex
Normal file
12
lib/mv_web/emails/email_layout_view.ex
Normal 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
|
||||||
13
lib/mv_web/emails/emails_view.ex
Normal file
13
lib/mv_web/emails/emails_view.ex
Normal 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
|
||||||
33
lib/mv_web/emails/join_confirmation_email.ex
Normal file
33
lib/mv_web/emails/join_confirmation_email.ex
Normal 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
|
||||||
18
lib/mv_web/templates/emails/join_confirmation.html.heex
Normal file
18
lib/mv_web/templates/emails/join_confirmation.html.heex
Normal 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>
|
||||||
33
lib/mv_web/templates/emails/layouts/layout.html.heex
Normal file
33
lib/mv_web/templates/emails/layouts/layout.html.heex
Normal 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>
|
||||||
18
lib/mv_web/templates/emails/password_reset.html.heex
Normal file
18
lib/mv_web/templates/emails/password_reset.html.heex
Normal 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>
|
||||||
16
lib/mv_web/templates/emails/user_confirmation.html.heex
Normal file
16
lib/mv_web/templates/emails/user_confirmation.html.heex
Normal 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>
|
||||||
1
mix.exs
1
mix.exs
|
|
@ -65,6 +65,7 @@ defmodule Mv.MixProject do
|
||||||
app: false,
|
app: false,
|
||||||
compile: false,
|
compile: false,
|
||||||
depth: 1},
|
depth: 1},
|
||||||
|
{:phoenix_swoosh, "~> 1.0"},
|
||||||
{:swoosh, "~> 1.16"},
|
{:swoosh, "~> 1.16"},
|
||||||
{:req, "~> 0.5"},
|
{:req, "~> 0.5"},
|
||||||
{:telemetry_metrics, "~> 1.0"},
|
{:telemetry_metrics, "~> 1.0"},
|
||||||
|
|
|
||||||
1
mix.lock
1
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_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_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_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_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"},
|
"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"},
|
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||||
|
|
|
||||||
|
|
@ -3249,3 +3249,78 @@ msgstr[1] "%{count} Filter aktiv"
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "without %{name}"
|
msgid "without %{name}"
|
||||||
msgstr "ohne %{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."
|
||||||
|
|
|
||||||
|
|
@ -3249,3 +3249,78 @@ msgstr[1] ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "without %{name}"
|
msgid "without %{name}"
|
||||||
msgstr ""
|
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 ""
|
||||||
|
|
|
||||||
|
|
@ -3249,3 +3249,78 @@ msgstr[1] "%{count} filters active"
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "without %{name}"
|
msgid "without %{name}"
|
||||||
msgstr "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 ""
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ defmodule Mv.Membership.JoinRequestSubmitEmailTest do
|
||||||
|
|
||||||
assert_email_sent(fn email_sent ->
|
assert_email_sent(fn email_sent ->
|
||||||
to_addresses = Enum.map(email_sent.to, &elem(&1, 1))
|
to_addresses = Enum.map(email_sent.to, &elem(&1, 1))
|
||||||
|
|
||||||
to_string(email) in to_addresses and
|
to_string(email) in to_addresses and
|
||||||
(email_sent.html_body =~ "/confirm_join/" or email_sent.text_body =~ "/confirm_join/")
|
(email_sent.html_body =~ "/confirm_join/" or email_sent.text_body =~ "/confirm_join/")
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,6 @@ defmodule MvWeb.JoinConfirmControllerTest do
|
||||||
conn = get(conn, "/confirm_join/public-test-token")
|
conn = get(conn, "/confirm_join/public-test-token")
|
||||||
|
|
||||||
assert conn.status == 200
|
assert conn.status == 200
|
||||||
refute redirected_to(conn) =~ "/sign-in"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue