feat: prevent join requests with equal mail
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
40a4461d23
commit
086ecdcb1b
22 changed files with 534 additions and 11 deletions
42
lib/mv_web/emails/join_already_member_email.ex
Normal file
42
lib/mv_web/emails/join_already_member_email.ex
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
defmodule MvWeb.Emails.JoinAlreadyMemberEmail do
|
||||
@moduledoc """
|
||||
Sends an email when someone submits the join form with an address that is already a member.
|
||||
|
||||
Used for anti-enumeration: the UI shows the same success message; only the email
|
||||
informs the recipient. Uses the unified email layout.
|
||||
"""
|
||||
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
|
||||
|
||||
@doc """
|
||||
Sends the "already a member" notice to the given address.
|
||||
|
||||
Returns `{:ok, email}` on success, `{:error, reason}` on delivery failure.
|
||||
"""
|
||||
def send(email_address) when is_binary(email_address) do
|
||||
subject = gettext("Membership application – already a member")
|
||||
|
||||
assigns = %{
|
||||
subject: subject,
|
||||
app_name: Mailer.mail_from() |> elem(0),
|
||||
locale: Gettext.get_locale(MvWeb.Gettext)
|
||||
}
|
||||
|
||||
email =
|
||||
new()
|
||||
|> from(Mailer.mail_from())
|
||||
|> to(email_address)
|
||||
|> subject(subject)
|
||||
|> put_view(MvWeb.EmailsView)
|
||||
|> render_body("join_already_member.html", assigns)
|
||||
|
||||
Mailer.deliver(email, Mailer.smtp_config())
|
||||
end
|
||||
end
|
||||
43
lib/mv_web/emails/join_already_pending_email.ex
Normal file
43
lib/mv_web/emails/join_already_pending_email.ex
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
defmodule MvWeb.Emails.JoinAlreadyPendingEmail do
|
||||
@moduledoc """
|
||||
Sends an email when someone submits the join form with an address that already
|
||||
has a submitted (confirmed) application under review.
|
||||
|
||||
Used for anti-enumeration: the UI shows the same success message; only the email
|
||||
informs the recipient. Uses the unified email layout.
|
||||
"""
|
||||
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
|
||||
|
||||
@doc """
|
||||
Sends the "application already under review" notice to the given address.
|
||||
|
||||
Returns `{:ok, email}` on success, `{:error, reason}` on delivery failure.
|
||||
"""
|
||||
def send(email_address) when is_binary(email_address) do
|
||||
subject = gettext("Membership application – already under review")
|
||||
|
||||
assigns = %{
|
||||
subject: subject,
|
||||
app_name: Mailer.mail_from() |> elem(0),
|
||||
locale: Gettext.get_locale(MvWeb.Gettext)
|
||||
}
|
||||
|
||||
email =
|
||||
new()
|
||||
|> from(Mailer.mail_from())
|
||||
|> to(email_address)
|
||||
|> subject(subject)
|
||||
|> put_view(MvWeb.EmailsView)
|
||||
|> render_body("join_already_pending.html", assigns)
|
||||
|
||||
Mailer.deliver(email, Mailer.smtp_config())
|
||||
end
|
||||
end
|
||||
|
|
@ -18,10 +18,16 @@ defmodule MvWeb.Emails.JoinConfirmationEmail do
|
|||
Uses the same SMTP configuration as the test mail (Settings or boot ENV) via
|
||||
`Mailer.deliver/2` with `Mailer.smtp_config/0` for consistency.
|
||||
|
||||
Called from the domain after a JoinRequest is created (submit flow).
|
||||
Called from the domain after a JoinRequest is created (submit flow) or when
|
||||
resending to an existing pending request.
|
||||
|
||||
## Options
|
||||
- `:resend` - If true, adds a short note that the link is being sent again for an existing request.
|
||||
|
||||
Returns `{:ok, email}` on success, `{:error, reason}` on delivery failure.
|
||||
"""
|
||||
def send(email_address, token) when is_binary(email_address) and is_binary(token) do
|
||||
def send(email_address, token, opts \\ [])
|
||||
when is_binary(email_address) and is_binary(token) do
|
||||
confirm_url = url(~p"/confirm_join/#{token}")
|
||||
subject = gettext("Confirm your membership request")
|
||||
|
||||
|
|
@ -29,7 +35,8 @@ defmodule MvWeb.Emails.JoinConfirmationEmail do
|
|||
confirm_url: confirm_url,
|
||||
subject: subject,
|
||||
app_name: Mailer.mail_from() |> elem(0),
|
||||
locale: Gettext.get_locale(MvWeb.Gettext)
|
||||
locale: Gettext.get_locale(MvWeb.Gettext),
|
||||
resend: Keyword.get(opts, :resend, false)
|
||||
}
|
||||
|
||||
email =
|
||||
|
|
|
|||
|
|
@ -21,9 +21,24 @@ defmodule MvWeb.JoinRequestLive.Helpers do
|
|||
@doc """
|
||||
Returns the reviewer display string (e.g. email) for a join request, or nil if none.
|
||||
|
||||
Accepts a join request struct or map with optional :reviewed_by_user (loaded User struct).
|
||||
Prefers the denormalized :reviewed_by_display (set on approve/reject) so the UI
|
||||
works for all roles without loading the User resource. Falls back to
|
||||
:reviewed_by_user when loaded (e.g. admin or legacy data before backfill).
|
||||
"""
|
||||
def reviewer_display(req) when is_map(req) do
|
||||
case Map.get(req, :reviewed_by_display) do
|
||||
s when is_binary(s) ->
|
||||
trimmed = String.trim(s)
|
||||
if trimmed == "", do: reviewer_display_from_user(req), else: trimmed
|
||||
|
||||
_ ->
|
||||
reviewer_display_from_user(req)
|
||||
end
|
||||
end
|
||||
|
||||
def reviewer_display(_), do: nil
|
||||
|
||||
defp reviewer_display_from_user(req) do
|
||||
user = Map.get(req, :reviewed_by_user)
|
||||
|
||||
case user do
|
||||
|
|
@ -42,6 +57,4 @@ defmodule MvWeb.JoinRequestLive.Helpers do
|
|||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def reviewer_display(_), do: nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -264,11 +264,16 @@ defmodule MvWeb.JoinRequestLive.Show do
|
|||
defp format_applicant_value(nil), do: nil
|
||||
defp format_applicant_value(""), do: nil
|
||||
defp format_applicant_value(%Date{} = date), do: DateFormatter.format_date(date)
|
||||
defp format_applicant_value(value) when is_map(value), do: format_applicant_value_from_map(value)
|
||||
|
||||
defp format_applicant_value(value) when is_map(value),
|
||||
do: format_applicant_value_from_map(value)
|
||||
|
||||
defp format_applicant_value(value) when is_boolean(value),
|
||||
do: if(value, do: gettext("Yes"), else: gettext("No"))
|
||||
|
||||
defp format_applicant_value(value) when is_binary(value) or is_number(value),
|
||||
do: to_string(value)
|
||||
|
||||
defp format_applicant_value(value), do: to_string(value)
|
||||
|
||||
defp format_applicant_value_from_map(value) do
|
||||
|
|
@ -283,8 +288,10 @@ defmodule MvWeb.JoinRequestLive.Show do
|
|||
end
|
||||
|
||||
defp format_applicant_value_simple(raw, _value) when is_binary(raw), do: raw
|
||||
|
||||
defp format_applicant_value_simple(raw, _value) when is_boolean(raw),
|
||||
do: if(raw, do: gettext("Yes"), else: gettext("No"))
|
||||
|
||||
defp format_applicant_value_simple(raw, _value) when is_integer(raw), do: to_string(raw)
|
||||
defp format_applicant_value_simple(_raw, value), do: to_string(value)
|
||||
|
||||
|
|
|
|||
10
lib/mv_web/templates/emails/join_already_member.html.heex
Normal file
10
lib/mv_web/templates/emails/join_already_member.html.heex
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<div style="color: #111827;">
|
||||
<p style="margin: 0 0 16px; font-size: 16px; line-height: 1.5;">
|
||||
{gettext(
|
||||
"We have received your request. The email address you entered is already registered as a member."
|
||||
)}
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 14px; color: #6b7280;">
|
||||
{gettext("If you have any questions, please contact us.")}
|
||||
</p>
|
||||
</div>
|
||||
10
lib/mv_web/templates/emails/join_already_pending.html.heex
Normal file
10
lib/mv_web/templates/emails/join_already_pending.html.heex
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<div style="color: #111827;">
|
||||
<p style="margin: 0 0 16px; font-size: 16px; line-height: 1.5;">
|
||||
{gettext(
|
||||
"We have received your request. You already have a membership application that is being reviewed."
|
||||
)}
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 14px; color: #6b7280;">
|
||||
{gettext("If you have any questions, please contact us.")}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
<div style="color: #111827;">
|
||||
<%= if @resend do %>
|
||||
<p style="margin: 0 0 16px; font-size: 16px; line-height: 1.5;">
|
||||
{gettext("You already had a pending request. Here is a new confirmation link.")}
|
||||
</p>
|
||||
<% end %>
|
||||
<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."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue