# Phase 15 — API Gateway Authentication & Rate Limiting

**Goal:** Wire API key validation into the live request pipeline so every inbound
request is authenticated before routing. Add per-tenant and per-key rate limiting
with an ETS token bucket, and extend the admin UI to show per-key usage metrics.

---

## Context

API keys exist in the DB (`api_keys` table) and are fully manageable through the
admin UI (Phase 13), but the gateway pipeline currently accepts requests from any caller
with no credential check. This phase closes that gap and adds the first layer of
tenant-level request governance.

**Flow after this phase:**
```
Client → POST /api/v1/:tenant_id/messages
          ├─ [NEW] ApiKeyAuth.check()   ← extract Bearer/header, hash, DB lookup
          ├─ [NEW] RateLimit.check()    ← per-key token bucket in ETS
          ├─ SchemaValidator.check()
          ├─ resolve_route()
          ├─ CircuitBreaker.check()
          ├─ IdempotencyPlug.check()
          └─ do_dispatch()
```

---

## Phases

### Phase 15-0 — API key auth plug

**New file:** `apps/mw_router/lib/mw_router/plugs/api_key_auth.ex`

```
ApiKeyAuth.check(ctx)
  → extract key from Authorization: Bearer mpk_xxx_... header
  → hash full key with :crypto.hash(:sha256, key)
  → ETS cache lookup (miss → DB query → ETS write with 5min TTL)
  → validate: active == true, tenant_id matches ctx.tenant_id, not expired
  → on pass: ctx = %{ctx | api_key_id: key.id, key_roles: key.roles}
  → on fail: {:error, :unauthorized, "Invalid or missing API key"}
```

**ETS table:** `:api_key_cache`
- Key: `key_hash` (binary)
- Value: `{%ApiKey{}, cached_at_monotonic}`
- TTL: 5 minutes (checked on read, no background sweep needed)

Bypass: requests from internal adapters and test environments pass a
`X-Internal-Token` header (hashed against `config :mw_router, internal_secret`).

**Wire into pipeline:**
File: `apps/mw_router/lib/mw_router/pipeline.ex`
Add as first step in `run/1` before `SchemaValidator.check()`.

---

### Phase 15-1 — Rate limiting

**New file:** `apps/mw_router/lib/mw_router/plugs/rate_limiter.ex`

Replace or extend the existing `RateLimiter` stub with a real token bucket:

```
RateLimiter.check(ctx)
  → bucket key: {tenant_id, key_id}   ← per-key isolation
  → ETS table: :rate_limit_buckets
  → token bucket algorithm:
      tokens_left = bucket.tokens - 1 + refill(elapsed_ms, rate)
      if tokens_left < 0: {:error, :rate_limited, "Too many requests"}
      else: update ETS, {:ok, ctx}
  → limits loaded from tenant plan:
      standard:     100 req/min
      professional: 1000 req/min
      enterprise:   10000 req/min
```

Limit config stored in `config :mw_router, :rate_limits`.

**Headers added to response on pass:**
- `X-RateLimit-Limit: 100`
- `X-RateLimit-Remaining: 47`
- `X-RateLimit-Reset: 1746182400`

---

### Phase 15-2 — Usage counters

**New file:** `apps/mw_router/lib/mw_router/plugs/usage_tracker.ex`

After each successful dispatch, emit a telemetry event:
```elixir
:telemetry.execute([:mw_router, :api_key, :request], %{count: 1}, %{
  key_id: ctx.api_key_id,
  tenant_id: ctx.tenant_id,
  status: result_status
})
```

**New handler in `infra_telemetry`:** accumulates per-key counters in ETS:
```
:key_usage  →  %{key_id => %{requests: N, errors: N, last_used_at: DateTime}}
```

Persisted to DB every 5 minutes via a `GenServer` flush process:
```
apps/mw_router/lib/mw_router/usage_flusher.ex
```

**New migration:** `20260502000009_create_api_key_usage_snapshots.exs`
```sql
CREATE TABLE api_key_usage_snapshots (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  api_key_id BIGINT NOT NULL,
  tenant_id VARCHAR(128) NOT NULL,
  window_start DATETIME NOT NULL,
  window_end   DATETIME NOT NULL,
  request_count INT DEFAULT 0,
  error_count   INT DEFAULT 0,
  INDEX (api_key_id, window_start)
)
```

---

### Phase 15-3 — Admin UI enhancements

#### API Keys LiveView — usage column

File: `apps/gateway_web/lib/gateway_web_web/live/api_keys_live.ex`

On mount, load live ETS counter for each displayed key and add to socket assigns.
Add column "Requests (session)" to the keys table.

On `select_key` (new event), load 7-day hourly usage from `api_key_usage_snapshots`
and show a sparkline in the row expansion (ASCII bar chart in HTML).

#### Monitoring LiveView — Rate Limit dashboard

File: `apps/gateway_web/lib/gateway_web_web/live/monitoring_live.ex`

New section: **Rate Limit Activity**
- Table: Tenant | Key | Requests/min | Throttled | Last request
- Highlight rows where throttled > 0 in the last minute

PubSub subscription to `mw_router:rate_limited` events for live updates.

---

### Phase 15-4 — Route protection modes

File: `apps/infra_repo/lib/infra_repo/schemas/route_rule.ex`

Add `auth_mode` field:
- `"none"` — public route (default, preserves backward compatibility)
- `"api_key"` — requires valid API key
- `"api_key_tenant"` — key must match the request's tenant_id

File: `apps/gateway_web/lib/gateway_web_web/live/route_editor_live.ex`

Add auth mode dropdown to the route edit modal.

Pipeline checks `ctx.route_spec.auth_mode` to decide whether to run `ApiKeyAuth`.

---

## File change summary

| Action | File |
|--------|------|
| NEW | `apps/mw_router/lib/mw_router/plugs/api_key_auth.ex` |
| NEW | `apps/mw_router/lib/mw_router/plugs/rate_limiter.ex` (replace stub) |
| NEW | `apps/mw_router/lib/mw_router/plugs/usage_tracker.ex` |
| NEW | `apps/mw_router/lib/mw_router/usage_flusher.ex` |
| NEW | `apps/infra_repo/priv/repo/migrations/20260502000009_create_api_key_usage_snapshots.exs` |
| MOD | `apps/mw_router/lib/mw_router/pipeline.ex` — wire auth + rate limit |
| MOD | `apps/infra_repo/lib/infra_repo/schemas/route_rule.ex` — add auth_mode |
| MOD | `apps/gateway_web/lib/gateway_web_web/live/api_keys_live.ex` — usage column |
| MOD | `apps/gateway_web/lib/gateway_web_web/live/monitoring_live.ex` — rate limit section |
| MOD | `apps/gateway_web/lib/gateway_web_web/live/route_editor_live.ex` — auth mode dropdown |

---

## Rollout / backward compatibility

- `auth_mode` defaults to `"none"` — all existing routes continue to work without a key
- Existing internal adapter tests pass a configurable bypass header
- Feature can be enabled per-route without touching other routes

---

## Out of scope

- OAuth 2.0 / JWT bearer tokens — future phase
- IP allowlist per key — future phase
- Per-endpoint (path-level) rate limits — future phase
