Implement v1 URL shortener (Bearer API, public 302, landing, CI) (#2)
Deploy Beta / unit-tests (push) Successful in 4s
Deploy Beta / docker (push) Successful in 13s
Deploy Beta / deploy-beta (push) Successful in 2m38s

## Summary

- Standalone Django 6 shortener: Bearer `/api/links/` (create/list/detail/disable) and public `GET /<code>` 302
- Host split, target-host allowlist, named rotatable tokens; `short_url` from `PUBLIC_SHORT_URL`
- Landing page, DEBUG-only `/debug/` mint form, Django admin
- Docker/compose (host **8005**), Gitea CI like monica_site (PR tests, beta on merge, prod button)
- Caller contract in `API.md`

Closes #1. Infra follow-up: [server-infra#22](ai_ml_operations/server-infra#22).

## Test plan
- [ ] `cd site && uv run python manage.py test`
- [ ] `docker compose up --build` → http://127.0.0.1:8005/
- [ ] `POST /api/links/` with `Bearer monica:dev-only-token` → 201
- [ ] `GET /<code>` → 302 to allowlisted https URL
- [ ] No Bearer → 401; non-allowlisted host → 400
- [ ] `/debug/` only when `DEBUG=true`

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-08-30 04:55:33 -07:00
parent 4baaa4b33c
commit 630b770c12
48 changed files with 3469 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
+9
View File
@@ -0,0 +1,9 @@
"""ASGI config for shortener."""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shortener.settings")
application = get_asgi_application()
+12
View File
@@ -0,0 +1,12 @@
"""Load environment-specific Django settings based on DJANGO_ENV."""
import os
_environment = os.environ.get("DJANGO_ENV", "dev").lower()
if _environment == "prod":
from .prod import * # noqa: F403
elif _environment == "beta":
from .beta import * # noqa: F403
else:
from .dev import * # noqa: F403
+198
View File
@@ -0,0 +1,198 @@
"""Shared Django settings for all environments."""
import json
import os
from pathlib import Path
from urllib.parse import urlparse
BASE_DIR = Path(__file__).resolve().parent.parent.parent
def env(key: str, default: str | None = None) -> str | None:
return os.environ.get(key, default)
def env_bool(key: str, default: bool = False) -> bool:
value = os.environ.get(key)
if value is None:
return default
return value.lower() in {"1", "true", "yes", "on"}
def env_list(key: str, default: str = "") -> list[str]:
value = os.environ.get(key, default)
if not value:
return []
value = value.strip()
if value.startswith("["):
try:
parsed = json.loads(value)
except ValueError:
parsed = None
if isinstance(parsed, list):
return [str(item).strip() for item in parsed if str(item).strip()]
return [item.strip() for item in value.split(",") if item.strip()]
def env_int(key: str, default: int) -> int:
value = env(key)
if value is None or value == "":
return default
return int(value)
def parse_api_tokens(raw: str) -> list[tuple[str, str]]:
"""Parse ``name:secret,name:secret`` into ``[(name, secret), ...]``."""
tokens: list[tuple[str, str]] = []
if not raw:
return tokens
for part in raw.split(","):
part = part.strip()
if not part or ":" not in part:
continue
name, secret = part.split(":", 1)
name, secret = name.strip(), secret.strip()
if name and secret:
tokens.append((name, secret))
return tokens
def database_config() -> dict:
database_url = env("DATABASE_URL")
if database_url:
parsed = urlparse(database_url)
return {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": parsed.path.lstrip("/"),
"USER": parsed.username or "",
"PASSWORD": parsed.password or "",
"HOST": parsed.hostname or "",
"PORT": str(parsed.port or 5432),
}
}
return {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
SECRET_KEY = env(
"DJANGO_SECRET_KEY",
"django-insecure-dev-only-change-me-before-production",
)
DEBUG = env_bool("DJANGO_DEBUG", False)
allowed_hosts = env_list(
"DJANGO_ALLOWED_HOSTS",
"localhost,127.0.0.1,0.0.0.0,testserver,web,url-shortener,go.mkdrealtor.com",
)
ALLOWED_HOSTS = allowed_hosts if allowed_hosts else ["*"]
INSTALLED_APPS = [
"core.apps.CoreConfig",
"links.apps.LinksConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"whitenoise.runserver_nostatic",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"links.middleware.HostSplitMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "shortener.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"core.context_processors.branding",
],
},
},
]
WSGI_APPLICATION = "shortener.wsgi.application"
DATABASES = database_config()
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "America/Chicago"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.memory.InMemoryStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# --- Shortener ---
SHORT_DOMAIN = env("SHORT_DOMAIN", "localhost:8000") or "localhost:8000"
PUBLIC_SHORT_URL = (env("PUBLIC_SHORT_URL", "https://go.mkdrealtor.com") or "").rstrip(
"/"
)
SHORT_PUBLIC_HOSTS = env_list("SHORT_PUBLIC_HOSTS", SHORT_DOMAIN.split(":")[0])
SHORT_API_HOSTS = env_list(
"SHORT_API_HOSTS",
"localhost,127.0.0.1,0.0.0.0,testserver,web,url-shortener",
)
# Django admin — local/dev only. Never put the public API hostname here.
SHORT_ADMIN_HOSTS = env_list("SHORT_ADMIN_HOSTS", "localhost,127.0.0.1")
SHORTENER_API_TOKENS = parse_api_tokens(env("SHORTENER_API_TOKENS", "") or "")
SHORT_ALLOWED_HOSTS = env_list(
"SHORT_ALLOWED_HOSTS", "mkdrealtor.com,aimloperations.com"
)
SHORT_CODE_LENGTH = env_int("SHORT_CODE_LENGTH", 6)
CLICK_IP_PEPPER = env("CLICK_IP_PEPPER", "") or ""
CODE_ALPHABET = "23456789abcdefghjkmnpqrstuvwxyz"
SITE_NAME = env("SITE_NAME", "URL Shortening Service") or "URL Shortening Service"
CREDIT_NAME = env("CREDIT_NAME", "AI ML Operations") or "AI ML Operations"
CREDIT_URL = env("CREDIT_URL", "https://aimloperations.com") or "https://aimloperations.com"
+18
View File
@@ -0,0 +1,18 @@
"""Beta/staging settings."""
from .base import * # noqa: F403
from .logging import build_logging_config, logging_level_for_env
DEBUG = env_bool("DJANGO_DEBUG", False) # noqa: F405
if DEBUG:
import warnings
warnings.warn("DEBUG is enabled in beta environment.", stacklevel=1)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
LOGGING = build_logging_config(logging_level_for_env("beta"), "beta")
+17
View File
@@ -0,0 +1,17 @@
"""Development settings."""
from .base import * # noqa: F403
from .logging import build_logging_config, logging_level_for_env
DEBUG = True
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.memory.InMemoryStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
LOGGING = build_logging_config(logging_level_for_env("dev"), "dev")
+82
View File
@@ -0,0 +1,82 @@
"""Environment-specific logging configuration."""
import os
def build_logging_config(level: str, environment: str) -> dict:
"""Return a Django LOGGING dict for the given level and environment name."""
return {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": (
f"{{levelname}} {{asctime}} {{name}} {{filename}}:{{lineno}} "
f"{{process:d}} {{thread:d}} [env={environment}] {{message}}"
),
"style": "{",
},
"simple": {
"format": (
f"{{levelname}} [env={environment}] "
f"{{filename}}:{{lineno}} {{message}}"
),
"style": "{",
},
},
"filters": {
"strip_authorization": {
"()": "shortener.settings.logging.StripAuthorizationFilter",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose" if environment == "dev" else "simple",
"filters": ["strip_authorization"],
},
},
"root": {
"handlers": ["console"],
"level": level,
},
"loggers": {
"django": {
"handlers": ["console"],
"level": level,
"propagate": False,
},
"django.request": {
"handlers": ["console"],
"level": "ERROR" if environment == "prod" else level,
"propagate": False,
},
"django.server": {
"handlers": ["console"],
"level": level,
"propagate": False,
},
},
}
class StripAuthorizationFilter:
"""Drop log records that appear to contain an Authorization header."""
def filter(self, record) -> bool:
message = record.getMessage()
if "authorization" in message.lower() and "bearer" in message.lower():
return False
return True
def logging_level_for_env(environment: str) -> str:
override = os.environ.get("DJANGO_LOG_LEVEL")
if override:
return override.upper()
if environment == "dev":
return "DEBUG"
if environment == "beta":
return "INFO"
return "WARNING"
+16
View File
@@ -0,0 +1,16 @@
"""Production settings."""
from .base import * # noqa: F403
from .logging import build_logging_config, logging_level_for_env
DEBUG = False
if not env("DJANGO_SECRET_KEY"): # noqa: F405
raise ValueError("DJANGO_SECRET_KEY must be set in production.")
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
LOGGING = build_logging_config(logging_level_for_env("prod"), "prod")
+17
View File
@@ -0,0 +1,17 @@
"""URL configuration for shortener."""
from django.contrib import admin
from django.urls import include, path
from core.views import healthz, landing
from links.debug_views import debug_create
from links.views import redirect_view
urlpatterns = [
path("", landing, name="landing"),
path("healthz/", healthz, name="healthz"),
path("debug/", debug_create, name="debug-create"),
path("admin/", admin.site.urls),
path("api/links/", include("links.urls")),
path("<str:code>", redirect_view, name="redirect"),
]
+9
View File
@@ -0,0 +1,9 @@
"""WSGI config for shortener."""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shortener.settings")
application = get_wsgi_application()