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. This is the one piece of
wiring squatch_mail_dashboard genuinely cannot set up for you: by the
time a router (this macro included) sees a request, the host application's
own endpoint has already run Plug.Parsers and discarded the raw body.
Plug.Parsers's :body_reader option has no per-route scoping — it's
endpoint-wide — so there is no router-level fix.
You must add a path-conditional body_reader to your own endpoint,
before the router plug, that delegates to
SquatchMail.SNS.RawBodyReader only for the webhook path and falls
through to the plain reader for everything else (including the rest of
the dashboard, which doesn't need this):
# in your endpoint.ex
defmodule MyAppWeb.CacheBodyReader do
def read_body(conn, opts) do
if match?(["squatch", "webhooks", "sns", _token], conn.path_info) do
SquatchMail.SNS.RawBodyReader.read_body(conn, opts)
else
Plug.Conn.read_body(conn, opts)
end
end
end
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library(),
body_reader: {MyAppWeb.CacheBodyReader, :read_body, []}Adjust the path prefix if you mount the dashboard somewhere other than
/squatch. dev.exs and test/support/web_endpoint.ex in this repo carry
this exact pattern (SquatchMailDev.CacheBodyReader /
SquatchMail.Test.CacheBodyReader) as reference implementations, and
test/squatch_mail/web/webhook_route_test.exs asserts the wiring
preserves bytes exactly. If this step is skipped, WebhookController
falls back to re-encoding conn.params as JSON — not byte-identical to
what SNS sent — so signature verification will fail on every request; see
SquatchMail.SNS.RawBodyReader's own moduledoc for the underlying 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.