Files
westfarn 433308d615
Deploy Beta / unit-tests (push) Successful in 4s
Deploy Beta / docker (push) Successful in 10s
Deploy Beta / deploy-beta (push) Successful in 1m3s
Serve /api/ on piha.lc / beta.piha.li (#8)
## Summary
- Serve `/api/` on `piha.lc` / `beta.piha.li` (Bearer still required).
- Drop `shortener.aimloperations.com` / `shortener-beta.aimloperations.com` from env examples and caller docs.
- `/admin/` stays 404 on the public host.

Closes #7.

## Test plan
- [ ] `POST https://beta.piha.li/api/links/` with valid Bearer → 201
- [ ] Same without Bearer → 401
- [ ] `GET https://beta.piha.li/<code>` still 302
- [ ] `/admin/` on beta.piha.li → 404
- [ ] Unit tests: `cd site && uv run python manage.py test`

Reviewed-on: #8
2026-08-30 17:27:40 -07:00

83 lines
2.3 KiB
Python

"""Host split: public short domain serves redirects + /api/; admin stays local."""
from __future__ import annotations
import logging
from django.conf import settings
from django.http import Http404, HttpRequest
logger = logging.getLogger(__name__)
def _normalize_host(host: str) -> str:
return host.split(":")[0].lower().rstrip(".")
def _host_in(host: str, configured: list[str]) -> bool:
needle = _normalize_host(host)
raw = host.lower()
for entry in configured:
if not entry:
continue
if raw == entry.lower() or needle == _normalize_host(entry):
return True
return False
def is_api_host(host: str) -> bool:
return _host_in(host, list(getattr(settings, "SHORT_API_HOSTS", []) or []))
def is_public_host(host: str) -> bool:
return _host_in(host, list(getattr(settings, "SHORT_PUBLIC_HOSTS", []) or []))
def is_admin_host(host: str) -> bool:
return _host_in(host, list(getattr(settings, "SHORT_ADMIN_HOSTS", []) or []))
def serves_api(host: str) -> bool:
"""Short domain and extra API hosts both serve /api/ (Bearer)."""
return is_api_host(host) or is_public_host(host)
class HostSplitMiddleware:
"""One public host: GET /<code> and /api/ (Bearer). Admin = local only.
Auth, not a second DNS name, keeps /api/ closed: missing/wrong Bearer is 401;
empty token list is 503.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request: HttpRequest):
path = request.path
if path in {"/healthz", "/healthz/"}:
return self.get_response(request)
host = request.get_host()
if path.startswith("/debug"):
if not settings.DEBUG or is_public_host(host):
raise Http404()
return self.get_response(request)
if path.startswith("/admin"):
if not is_admin_host(host):
raise Http404()
return self.get_response(request)
if path.startswith("/api/"):
if not serves_api(host):
logger.warning(
"blocked /api/ host=%s public=%s api=%s",
host,
is_public_host(host),
is_api_host(host),
)
raise Http404()
return self.get_response(request)