# `MishkaCmsCore.Runtime.Plugs.ApiCorsPlug`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/plugs/api_cors_plug.ex#L5)

CORS (Cross-Origin Resource Sharing) plug for API endpoints.

This plug handles CORS headers for API requests, comparing the request's `Origin` with the origins
the current site allows. It supports both preflight (OPTIONS) requests and regular API requests,
automatically setting the appropriate CORS headers based on the site configuration.

## What an origin is

An origin is a scheme, a host and a port, and all three have to be the same: `https://site.com`
does not allow `http://site.com` or `https://site.com:9999`, because a cookie is not kept apart by
scheme or port and the answer carries `access-control-allow-credentials: true`. The request's
`Origin` is read by `MishkaCmsCore.Runtime.OriginValidator.request_origin/1` and must be nothing
but those three.

## What a site allows

Worked out by `MishkaCmsCore.Runtime.OriginValidator.site_origins/1` when the site is saved, and
looked up in `MishkaCmsCore.Runtime.States.SiteState` (`allows_origin?/2`) for each request, so a
site created or edited while the server runs is answered for what it allows now. A loopback host is
`localhost`, a name under `.localhost`, `127.0.0.1` or `::1`.

| The site has | Allowed | Not allowed |
| --- | --- | --- |
| `host: "site.com"` | `https://site.com` | `http://site.com`, `https://site.com:8443`, `https://sub.site.com` |
| `host: "site-a.localhost"` (loopback) | `https://site-a.localhost`, `http://site-a.localhost` | `http://site-a.localhost:3000` |
| `frontend_domain: "app.example.com"` | `https://app.example.com` | `http://app.example.com`, `https://app.example.com:3000` |
| `frontend_domain: "app.example.com:3000"` | `https://app.example.com:3000` | `http://app.example.com:3000`, `https://app.example.com` |
| `frontend_domain: "localhost:5173"` (loopback) | `https://localhost:5173`, `http://localhost:5173` | `http://localhost:5174` |
| `frontend_domain: "https://app.example.com:8443"` | `https://app.example.com:8443` | `http://app.example.com:8443`, `https://app.example.com` |
| `allowed_origins: ["https://partner.com"]` | `https://partner.com`, `https://partner.com:443` | `http://partner.com`, `https://partner.com:8443` |
| `allowed_origins: ["http://other.com:8080/"]` | `http://other.com:8080` | `https://other.com:8080`, `http://other.com` |

Never allowed: a missing site, a `phoenix_only` site, more than one `Origin` header, `null`, and an
`Origin` with a path, a query, a fragment, credentials or a port that is not a number.

## Development

Compiled only where the app's config sets `:env_dev?`, a loopback origin on any port, over `http`
or `https`, is allowed for any site the rules above let answer at all — so a frontend on
`http://localhost:5173` reads the API of a site the database knows only by its public address. A
release has no such clause, and neither do the tests, which set `:env_test?` alone. Not
`Mix.env/0`: a dependency is compiled in `:prod` whatever the app runs in.

## The answer

An allowed origin gets `access-control-allow-origin` with the origin as a browser writes it,
never the request's own text, and `access-control-allow-credentials: true`; a preflight from it
gets the methods, headers and max age too and is answered `204`. A preflight from any other origin
is answered `204` with none of them. Every response that comes through here varies by `Origin`:
`Origin` is merged into the `vary` header already set or set later, once, whatever its case.

## Usage

    pipeline :api do
      plug :accepts, ["json"]
      plug MishkaCmsCore.Runtime.Plugs.ApiTenantLookupPlug
      plug MishkaCmsCore.Runtime.Plugs.ApiCorsPlug
    end

## Options

  * `:site` — the site to answer for, in place of `conn.assigns[:current_site]`.
    `MishkaCmsCore.Runtime.Plugs.ApiTenantLookupPlug` passes the switched-off site it refuses, so
    that site's preflight is answered and its `503` carries the headers an allowed origin needs to
    read it.

# `call`

# `init`

---

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