2026-02-28 23:25:28 +00:00
|
|
|
defmodule BerrypodWeb.NewsletterController do
|
|
|
|
|
use BerrypodWeb, :controller
|
|
|
|
|
|
|
|
|
|
alias Berrypod.Newsletter
|
|
|
|
|
|
2026-03-08 08:58:43 +00:00
|
|
|
plug BerrypodWeb.Plugs.RateLimit, [type: :newsletter] when action == :subscribe
|
|
|
|
|
|
2026-02-28 23:25:28 +00:00
|
|
|
@doc "No-JS fallback for newsletter signup form."
|
|
|
|
|
def subscribe(conn, %{"email" => email}) do
|
|
|
|
|
ip_hash = hash_ip(conn)
|
|
|
|
|
|
|
|
|
|
case Newsletter.subscribe(email,
|
|
|
|
|
consent_text: "Newsletter signup on website",
|
|
|
|
|
ip_hash: ip_hash
|
|
|
|
|
) do
|
|
|
|
|
{:ok, _} ->
|
|
|
|
|
conn
|
|
|
|
|
|> put_flash(:info, "Check your inbox to confirm your subscription.")
|
|
|
|
|
|> redirect(to: redirect_back(conn))
|
|
|
|
|
|
|
|
|
|
{:already_confirmed, _} ->
|
|
|
|
|
conn
|
|
|
|
|
|> put_flash(:info, "You're already subscribed!")
|
|
|
|
|
|> redirect(to: redirect_back(conn))
|
|
|
|
|
|
|
|
|
|
{:error, _} ->
|
|
|
|
|
conn
|
|
|
|
|
|> put_flash(:error, "Please enter a valid email address.")
|
|
|
|
|
|> redirect(to: redirect_back(conn))
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def subscribe(conn, _params) do
|
|
|
|
|
conn
|
|
|
|
|
|> put_flash(:error, "Please enter your email address.")
|
|
|
|
|
|> redirect(to: ~p"/")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc "Handles confirmation link from the double opt-in email."
|
|
|
|
|
def confirm(conn, %{"token" => token}) do
|
|
|
|
|
case Newsletter.confirm(token) do
|
|
|
|
|
{:ok, _sub} ->
|
|
|
|
|
conn
|
|
|
|
|
|> put_status(200)
|
|
|
|
|
|> html(confirmation_html(:success))
|
|
|
|
|
|
|
|
|
|
{:error, :invalid_token} ->
|
|
|
|
|
conn
|
|
|
|
|
|> put_status(400)
|
|
|
|
|
|> html(confirmation_html(:invalid))
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp confirmation_html(:success) do
|
|
|
|
|
shop_name = Berrypod.Settings.get_setting("shop_name", "Berrypod")
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
<title>Subscription confirmed</title>
|
|
|
|
|
<style>body{font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;padding:0 24px;color:#111}</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<h1 style="font-size:1.25rem;margin-bottom:0.75rem">You're subscribed!</h1>
|
|
|
|
|
<p style="color:#555">Thanks for confirming. You'll receive the #{shop_name} newsletter from now on.</p>
|
|
|
|
|
<p style="margin-top:1.5rem"><a href="/" style="color:#111">Back to the shop</a></p>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp confirmation_html(:invalid) do
|
|
|
|
|
"""
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
<title>Invalid link</title>
|
|
|
|
|
<style>body{font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;padding:0 24px;color:#111}</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<h1 style="font-size:1.25rem;margin-bottom:0.75rem">Link invalid or expired</h1>
|
|
|
|
|
<p style="color:#555">This confirmation link has expired or is invalid. Please try subscribing again.</p>
|
|
|
|
|
<p style="margin-top:1.5rem"><a href="/" style="color:#111">Back to the shop</a></p>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp redirect_back(conn) do
|
|
|
|
|
case get_req_header(conn, "referer") do
|
|
|
|
|
[referer | _] ->
|
|
|
|
|
uri = URI.parse(referer)
|
|
|
|
|
uri.path || "/"
|
|
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
|
"/"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp hash_ip(conn) do
|
|
|
|
|
daily_salt = Date.utc_today() |> Date.to_iso8601()
|
|
|
|
|
ip_string = conn.remote_ip |> :inet.ntoa() |> to_string()
|
|
|
|
|
:crypto.hash(:sha256, ip_string <> daily_salt) |> Base.encode16(case: :lower)
|
|
|
|
|
end
|
|
|
|
|
end
|