# `SquatchMail.Web.Router`
[🔗](https://github.com/Axio-Intelligence/Squatch-Mail/blob/v0.1.0/lib/squatch_mail/web/router.ex#L1)

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
    end

This 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]
    end

Passing `: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: true` in `dev.exs` to
    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.get` away
    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 of `on_mount` hooks run before SquatchMail's own.
    Also signals to layer (c) that the host is handling auth (see above).
  * `:as` - the `live_session` name. Defaults to `:squatch_mail_dashboard`.
    `live_session` names must be unique per router *module*, not per
    `scope` — if you mount `squatch_mail_dashboard` more than once in the
    same router (e.g. one internal, one customer-facing instance), give
    every mount after the first a distinct `:as`.

# `squatch_mail_dashboard`
*macro* 

Mounts the SquatchMail dashboard at `path`. See the moduledoc for the full
security model.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
