Mounts the SquatchMail dashboard in a host Phoenix router.
defmodule MyAppWeb.Router do
use MyAppWeb, :router
import SquatchMail.Web.Router
scope "/" do
pipe_through :browser
squatch_mail_dashboard "/squatch"
end
endThis expands to a Phoenix.LiveView.Router.live_session/3 wrapping seven
routes — Trail Log (/, the live activity feed), the Sightings archive
(/sightings), the Sighting inspector (/sightings/:public_id), Bounces
(/bounces), Complaints (/complaints), the Do-Not-Disturb registry
(/suppressions), and Base Camp (/base-camp) — plus a
GET .../activity/export.csv CSV
download (same auth as the dashboard), GET .../assets/* routes for
the dashboard's self-contained CSS/JS and logo (never auth-gated), and a
POST .../webhooks/sns/:token route that forwards to
SquatchMail.Web.WebhookController (authenticated by its token, not the
dashboard's auth layers).
Security
SquatchMail ships three layers of access control, in order of
precedence. Exactly one applies for any given request to a dashboard
route (Trail Log, Sightings, Suppressions, Base Camp); the SNS webhook
route is never covered by any of them — it authenticates itself via its
per-source :token path segment instead (signature verification happens
inside SquatchMail.Web.WebhookController).
All three layers are enforced by SquatchMail.Web.Plugs.Auth, a plain
Plug that runs before live_session in the ordinary Plug pipeline.
This is deliberate: HTTP Basic Auth's 401 challenge and the refusal page
in layer (c) both need to send a real HTTP status/headers before Phoenix
commits to rendering a LiveView, which is only possible from a Plug — a
LiveView on_mount hook only ever sees a Phoenix.LiveView.Socket and can
at most redirect, never issue an arbitrary status code
(see Phoenix.LiveView.Router's own documentation on this exact
limitation). on_mount remains the right place for post-auth concerns —
SquatchMail uses SquatchMail.Web.OnMount there only to assign
theme/config, never to gate access.
a) Recommended: host-owned authentication
Mount squatch_mail_dashboard inside your own authenticated/admin pipeline
and pass your own on_mount hooks, exactly like Oban Web or
Phoenix LiveDashboard:
scope "/" do
pipe_through [:browser, :require_admin_user]
squatch_mail_dashboard "/squatch", on_mount: [MyAppWeb.AdminAuth]
endPassing :on_mount tells SquatchMail.Web.Plugs.Auth that the host is
handling authorization itself further up the pipeline (in
:require_admin_user above), so layers (b) and (c) both stand down
regardless of their configuration. This is the only layer that can express
arbitrary authorization (roles, per-user scoping, SSO, etc.) — layers (b)
and (c) exist as a safety net for hosts that mount the dashboard without
wiring up their own auth, not as a replacement for it.
b) Built-in fallback: HTTP Basic Auth
If the host configures
config :squatch_mail,
basic_auth: [username: "squatch", password: System.fetch_env!("SQUATCH_MAIL_PASSWORD")]then every dashboard route (never the SNS webhook route) is protected by
Plug.BasicAuth with those credentials — this check takes precedence over
everything else, including a configured :on_mount, since configuring
:basic_auth is an explicit, unambiguous request for that gate.
c) Safe default: refuse in production
If neither (a) nor (b) applies — no :on_mount was given to
squatch_mail_dashboard and no :basic_auth is configured — SquatchMail
checks Application.get_env(:squatch_mail, :allow_unauthenticated, false).
- In development, hosts are expected to be running locally, so leaving
this unset is harmless and the dashboard mounts normally — set
config :squatch_mail, allow_unauthenticated: trueindev.exsto make that explicit. - When unset (the default) and neither (a) nor (b) applies, dashboard
routes instead render a refusal page explaining how to configure layer
(a) or (b). This is a deliberately conservative default: an embeddable
dashboard with no auth at all must never be one
mix deps.getaway from being reachable in production.
This check is a runtime Application.get_env/3 read, not Mix.env() —
Mix.env() does not exist in a compiled release, so gating on it would
silently disable the safety net in exactly the environment (production)
where it matters most.
Note that the refusal page's own CSS/JS still load: asset routes
(.../assets/css-*, .../assets/js-*) are mounted outside the scope this
plug covers, in any configuration, so the refusal page itself always
renders correctly instead of appearing broken.
Webhook raw body
SNS signature verification (SquatchMail.SNS.MessageVerifier, used by
SquatchMail.Web.WebhookController) needs the exact bytes SNS sent, not a
round-tripped re-encoding of the parsed params. squatch_mail_dashboard
handles this for you: the webhook route pipes through
SquatchMail.SNS.RawBodyPlug, which reads the raw body into
conn.assigns[:raw_body] before the controller runs. Hosts need to wire
up nothing — not a :body_reader, not an endpoint change.
This works even though the host endpoint's Plug.Parsers runs first,
because SNS delivers with Content-Type: text/plain; charset=UTF-8 and
Plug.Parsers (with the usual [:urlencoded, :multipart, :json] parsers
and pass: ["*/*"]) matches no parser for text/plain — so it passes the
request through with the body still unread, leaving those bytes for
RawBodyPlug to consume in SquatchMail's own pipeline. This is also why the
earlier "endpoint-wide :body_reader, no router-level fix" framing was
wrong for SNS: Plug.Parsers never invokes a :body_reader for
text/plain at all, so an endpoint reader would never have fired for a real
SNS request regardless.
Hosts that already capture the raw body themselves still work: if
conn.assigns[:raw_body] is already set (e.g. via
SquatchMail.SNS.RawBodyReader wired into the endpoint's Plug.Parsers
:body_reader), RawBodyPlug leaves it untouched. That path is an optional
belt-and-suspenders, no longer a requirement. If, somehow, the raw body is
not captured at all, WebhookController logs an actionable error and falls
back to re-encoding conn.params — not byte-identical to what SNS sent, so
signature verification will fail; see SquatchMail.SNS.RawBodyPlug's
moduledoc for the full detail.
Options
:on_mount- a list ofon_mounthooks run before SquatchMail's own. Also signals to layer (c) that the host is handling auth (see above).:as- thelive_sessionname. Defaults to:squatch_mail_dashboard.live_sessionnames must be unique per router module, not perscope— if you mountsquatch_mail_dashboardmore than once in the same router (e.g. one internal, one customer-facing instance), give every mount after the first a distinct:as.
Summary
Functions
Mounts the SquatchMail dashboard at path. See the moduledoc for the full
security model.