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

Site lookup plug for multi-site applications with host-based site resolution.

This plug identifies the current site based on the incoming HTTP Host header and stores
site information in both connection assigns and session for use throughout the request
lifecycle in controllers and LiveViews.

## Features

- Host-based site resolution with port normalization
- Automatic site state caching and retrieval via SiteState
- Master sessions: a master's own token leaves the master site's requests unscoped
- Tenant isolation using Ash framework integration
- Graceful error handling with custom 503 pages
- Session persistence for site information

## Workflow

1. **Host Extraction**: Extracts and normalizes host from request headers
2. **Site Lookup**: Queries SiteState cache for host-to-site mapping
3. **Site Validation**: Checks site active status
4. **Redirects**: An address one of the site's redirect rules names is answered with that redirect
   and halted, before the session is touched — see `MishkaCmsCore.Runtime.Redirects`
5. **Sign-in Pool**: On the master site, `?master_user=` picks the accounts its sign-in page uses
6. **Tenant Assignment**: Sets Ash tenant context from the session's token
7. **State Storage**: Persists site data in assigns and session

## Usage

Add it to the browser pipeline after the session is fetched and before `:load_from_session`, with
`confirm_tenant/2` straight after `:load_from_session`. The order is the rule: `:load_from_session`
verifies the session's token against the tenant this plug chose, and `confirm_tenant/2` checks the
user that verification loaded.

```elixir
import AshAuthentication.Plug.Helpers
import MishkaCmsCore.Runtime.Plugs.SiteLookupPlug, only: [confirm_tenant: 2]

pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :protect_from_forgery
  plug MishkaCmsCore.Runtime.Plugs.SiteLookupPlug
  plug :load_from_session
  plug :confirm_tenant
end

scope "/", MyAppWeb do
  pipe_through :browser
  # Your routes here
end
```

## Masters

A master is a user with no site. Whether a request belongs to one is read from the signed-in
user's own token, never from the address: a master's token is issued with no site, and
`:load_from_session` recognises it only on a request this plug left unscoped. The master site —
the one whose record has `master: true` — leaves a request unscoped for such a token; every other
site scopes every request to itself, so a master signs in on the master site's address and
nowhere else.

Before anyone is signed in, the master site's sign-in page is told which accounts to use:

```elixir
# The master accounts
"/auth?master_user=true"

# The site's own accounts
"/auth?master_user=false"
```

The choice is kept in the session as `:master_mode` and unscopes only the sign-in pages of a
visitor with no token — the addresses `sign_in_path?/2` names. It grants no access, a signed-in
user's token outranks it, and every other site ignores it and forgets any it finds, telling a
plugin on `web:request:master_sign_in_refused` with `%{site: site}`.

`confirm_tenant/2` runs after `:load_from_session` and holds the rest of the rule: an unscoped
request is a master's, or a visitor's on that sign-in page.

## Site Resolution Examples

```elixir
# Request: example.com -> Looks up site by "example.com"
# Request: example.com:4000 -> Normalizes to "example.com"
# Request: subdomain.example.com -> Looks up site by "subdomain.example.com"
```

## Error Handling

- **Site Not Found**: Returns 503 with "Service Unavailable" page
- **Inactive Site**: Returns 503 with "Site Temporarily Unavailable" page
- **Missing Host**: Treated as site not found scenario

## Integration with Other Components

- **SiteState**: Leverages runtime state management for fast site lookups
- **Ash Framework**: Automatic tenant context setting for multi-tenant data isolation
- **Session Management**: Persists site information across requests
- **LiveView**: Site information available in LiveView assigns

## Production Considerations

- Host header validation should be handled at load balancer level
- Site state caching reduces database queries for each request
- The master site's address should be known only to its masters: no other site links to it
- Error pages are cached and served efficiently

# `auth_address`

```elixir
@spec auth_address(String.t() | nil, String.t() | nil, String.t()) :: String.t()
```

The address of the site's sign-in page in `language`, or of the page `under` it — `/:lang/auth`
as `MishkaCmsCore.Runtime.Languages.url/3` builds it, so a visitor sent to sign in, or to sign
out, lands on the page in the language they were reading. A language the site does not publish
in, and `nil`, are the site's first.

Every place that sends a visitor to sign in asks this, with the language they read pages in: the
socket's for a runtime page, the session's (`MishkaCmsCore.Runtime.Languages.session/0`) anywhere
else.

    # site.languages ["en", "fa"], site.language_urls "prefix_others"
    SiteLookupPlug.auth_address("fa", site.id)                #=> "/fa/auth"
    SiteLookupPlug.auth_address(nil, site.id)                 #=> "/auth"
    SiteLookupPlug.auth_address("en", site.id, "/sign-out")   #=> "/auth/sign-out"

# `call`

Performs site lookup based on Host header and manages site context.

This is the main plug function that orchestrates the entire site resolution workflow:
1. Extracts and normalizes the host from request headers
2. Queries SiteState for site information by host
3. Validates site status and manages tenant context
4. Stores site information in connection assigns and session

## Parameters

- `conn` - The Plug.Conn struct representing the HTTP connection
- `opts` - Plug options (from init/1, currently unused)

## Returns

Modified `Plug.Conn` with:
- `:current_site` assign containing site data (if found and active)
- `:current_site` session data for persistence
- Ash tenant context set for multi-tenant data isolation
- HTTP 503 response for inactive or missing sites

## Examples

```elixir
# Successful site lookup
conn = SiteLookupPlug.call(conn, %{})
site = conn.assigns[:current_site]
# => %MishkaCms.Content.Site{host: "example.com", active: true, ...}

# Access in controller
def index(conn, _params) do
  site = conn.assigns[:current_site]
  render(conn, "index.html", site: site)
end

# Access in LiveView
def mount(_params, session, socket) do
  site = session["current_site"]
  {:ok, assign(socket, :site, site)}
end
```

## Response Scenarios

- **Active Site Found**: Connection continues with site context
- **Redirected Address**: The rule's status and `location`, halted, with `:current_site` assigned
- **Inactive Site**: 503 "Site Temporarily Unavailable" response
- **Site Not Found**: 503 "Service Unavailable" response
- **Master session on the master site**: Site context set, no tenant

# `confirm_tenant`

```elixir
@spec confirm_tenant(Plug.Conn.t(), term()) :: Plug.Conn.t()
```

Confirms the tenant `call/2` chose, once `:load_from_session` has read the session's user.

Plug it in straight after `:load_from_session`, imported from this module:

```elixir
import MishkaCmsCore.Runtime.Plugs.SiteLookupPlug, only: [confirm_tenant: 2]

pipeline :browser do
  # ...
  plug MishkaCmsCore.Runtime.Plugs.SiteLookupPlug
  plug :load_from_session
  plug :confirm_tenant
end
```

A request stays unscoped only for a master, a user whose `site_id` is nil, or for a visitor on the
master site's sign-in pages who asked for the master accounts. Anyone else on an unscoped request —
a user with a site, or one that carries no `site_id` at all — is signed out of it: `:current_user` becomes `nil`, the token leaves the session, the request is
scoped to the site, and a plugin on `web:request:session_refused` hears it with
`%{site: site, user_id: id}`. An unscoped request whose token did not verify is scoped to the site.

A user the site's closed sign-in signs out (`MishkaCmsCore.Accounts.AuthGate.signs_out?/2`) is
signed out everywhere, their session cleared, and sent to the front page with the reason.

# `init`

Initializes the plug with configuration options.

Currently accepts no specific configuration options and returns an empty map.
This allows for future extensibility without breaking existing implementations.

## Parameters

- `opts` - Plug initialization options (currently unused)

## Returns

Empty map `%{}` for plug state

## Examples

```elixir
# Standard usage in router
plug MishkaCmsCore.Runtime.Plugs.SiteLookupPlug

# With explicit options (currently unused)
plug MishkaCmsCore.Runtime.Plugs.SiteLookupPlug, %{}
```

# `sign_in_path?`

```elixir
@spec sign_in_path?([String.t()], %{id: String.t()} | nil) :: boolean()
```

Whether `segments` — a request path split into its segments, as `conn.path_info` holds it — is an
address a sign-in happens on for `site`: `/auth`, `/api/auth` and every address under them, and
`/<language>/auth` and every address under it when `<language>` is one of the site's languages
(`MishkaCmsCore.Runtime.Languages.site/1`). Whole segments only, so `/authors` is a page like any
other. `nil` for `site` recognises only the addresses without a language.

Every rule that treats the sign-in pages apart asks this: the master accounts' unscoped sign-in
here, `MishkaCms.Plugs.MaintenanceGate` and `MishkaCmsCore.Runtime.SiteAvailability` keeping them
open on an offline site, and `MishkaCmsWeb.RuntimeLive` loading the app's own client on them.

    SiteLookupPlug.sign_in_path?(["en", "auth", "register"], site) #=> true
    SiteLookupPlug.sign_in_path?(["api", "auth", "user"], site)    #=> true
    SiteLookupPlug.sign_in_path?(["xx", "auth"], site)             #=> false
    SiteLookupPlug.sign_in_path?(["authors"], site)                #=> false

# `site_for`

```elixir
@spec site_for(String.t() | nil) :: {:ok, map()} | {:error, :not_found}
```

The site a request's `host` names — the `Host` header without its port, as
`MishkaCmsCore.Runtime.OriginValidator.host_name/1` reads it — as
`MishkaCmsCore.Runtime.States.SiteState.get_by_host/1` answers.

In a build whose config sets `:env_dev?` or `:env_test?`, `localhost` and `127.0.0.1` are the
master site while no site has that host. A release has no such clause: there they name the site
that has them, as every other host does, or none.

This plug and `MishkaCmsCore.Runtime.Plugs.ApiTenantLookupPlug` both resolve a request's site
through it, so a page and an API call on the same address reach the same site.

    SiteLookupPlug.site_for("blog.example.com")  #=> {:ok, %{host: "blog.example.com", ...}}
    SiteLookupPlug.site_for("localhost")         #=> {:ok, %{master: true, ...}} in dev and test
    SiteLookupPlug.site_for("unknown.example")   #=> {:error, :not_found}

---

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