LAN admin on 10.0.0.128 plus quick campaign mint form (#12)
Deploy Beta / unit-tests (push) Successful in 4s
Deploy Beta / docker (push) Successful in 10s
Deploy Beta / deploy-beta (push) Successful in 53s

Closes #11.

## Summary
- Serve Django admin on LAN IP `10.0.0.128` (not `piha.li`). Compose now publishes `0.0.0.0:8005` and passes `SHORT_ADMIN_HOSTS` into the container.
- Admin index has a campaign mint form: domain, campaign, source, metric. Save builds `https://{domain}/?utm_campaign=&utm_source=&utm_medium=` (metric) and shows a copyable short URL.
- Public `/admin/` on `piha.lc` / `piha.li` stays 404.

## Test plan
- [ ] `cd site && uv run python manage.py test`
- [ ] Recreate compose (`docker compose up --build`) so `WEB_BIND` / `SHORT_ADMIN_HOSTS` take effect
- [ ] From another LAN machine: `http://10.0.0.128:8005/admin/` (staff login) shows the mint form
- [ ] Save a link for an allowlisted domain, copy the short URL, confirm it 302s
- [ ] `https://piha.li/admin` still 404

Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
2026-09-16 03:39:04 -07:00
parent dd627b75c9
commit 69d8b3e7a3
10 changed files with 351 additions and 16 deletions
+35 -1
View File
@@ -7,7 +7,7 @@ import hmac
import logging
import secrets
from datetime import datetime
from urllib.parse import urlsplit, urlunsplit
from urllib.parse import urlencode, urlsplit, urlunsplit
from django.conf import settings
from django.db import IntegrityError
@@ -45,6 +45,40 @@ def host_allowed(hostname: str, allowed: list[str]) -> bool:
return False
def normalize_destination_host(raw: str) -> str:
"""Strip scheme/path from a domain field. Raise ValidationError if empty."""
raw = (raw or "").strip()
if not raw:
raise ValidationError("invalid url")
if raw.startswith("//"):
raise ValidationError("invalid url")
if "://" not in raw:
raw = "https://" + raw
try:
parts = urlsplit(raw)
except ValueError as exc:
raise ValidationError("invalid url") from exc
hostname = (parts.hostname or "").lower().rstrip(".")
if not hostname:
raise ValidationError("invalid url")
if parts.username or parts.password:
raise ValidationError("invalid url")
return hostname
def build_tracked_url(*, domain: str, campaign: str, source: str, metric: str) -> str:
"""Build an allowlisted https URL with UTM query params."""
hostname = normalize_destination_host(domain)
query = urlencode(
{
"utm_campaign": campaign.strip(),
"utm_source": source.strip(),
"utm_medium": metric.strip(),
}
)
return validate_target_url(urlunsplit(("https", hostname, "/", query, "")))
def validate_target_url(raw: str) -> str:
"""Return a canonical https URL or raise ValidationError."""
if not raw or not isinstance(raw, str):