westfarn 143ac7c6d0
CI / test (pull_request) Successful in 6s
Add v1 shortener: Bearer API, public 302, landing, and CI.
Standalone Django service so callers can mint links and phones get a 302.
Closes #1.
2026-08-30 06:54:32 -05:00

url_shortening_service

Django 6 URL shortener. monica_site (and other trusted callers) mint links over Bearer auth on an API hostname (may be public DNS). SMS recipients hit GET /<code> on the short domain and get a 302 to the long HTTPS URL.

This service is standalone. Do not fold it into monica_site.

Two surfaces

Who Path Auth Host
Phone / public internet GET /<code> none SHORT_DOMAIN (NPM + TLS)
Anyone GET / none landing page on the short domain
monica_site /api/links/ Authorization: Bearer name:secret own DNS / NPM host — not the short domain

The API may be on the public internet. It is not open: every /api/ request needs a named Bearer token. No token / wrong token → 401. No tokens configured → 503. GET /<code> never requires a token.

/api/ and /admin/ are 404 on the short domain. /admin/ is also 404 on the public API hostname (localhost only). GET /debug/ is a mint form when DEBUG=true and never on the short domain.

Local run

cp .env.example .env
# SHORTENER_API_TOKENS=monica:dev-only-token is already set
docker compose up --build

Or without Docker:

uv sync
cp .env.example .env
cd site && uv run python manage.py migrate
uv run python manage.py runserver
# optional local admin:
# uv run python manage.py createsuperuser
# then http://127.0.0.1:8005/admin/  (compose) or :8000 (runserver)

Tests (SQLite, no network):

cd site && uv run python manage.py test

CI / deploy (Gitea)

Same split as monica_site:

Workflow When What
CI pull request → master unit tests
Deploy Beta push / merge to master unit tests → compose tests → deploy beta
Deploy Prod Actions → Run workflow (button) unit tests → compose tests → deploy prod

Deploy calls server-infra/scripts/deploy.sh --app url_shortening_service. Needs server-infra#22 first.

Use case

  1. Your site POSTs to this service with a Bearer token and a long HTTPS URL.
  2. Response 201 includes short_url built from PUBLIC_SHORT_URL (not the API Host).
  3. A person on the public internet opens that short URL.
  4. This service 302s them to the long URL and increments click_count.

Mint (API host / localhost):

curl -sS -X POST http://127.0.0.1:8005/api/links/ \
  -H "Authorization: Bearer monica:dev-only-token" \
  -H "Content-Type: application/json" \
  -d '{"target_url":"https://mkdrealtor.com/","title":"test"}'

Follow (no token — this is the public path):

curl -sSI http://127.0.0.1:8005/<code>

Expect HTTP/1.1 302 Found and Location: https://mkdrealtor.com/.

Caller integration (onboard + /api/links/): API.md.

Caller contract (monica_site, other repo)

SHORTENER_BASE_URL=https://shortener.aimloperations.com
SHORTENER_API_TOKEN=monica:<same-secret-as-SHORTENER_API_TOKENS>
POST /api/links/
Authorization: Bearer monica:<secret>
Content-Type: application/json

{"target_url":"https://mkdrealtor.com/listings/oak-st?utm_source=monica&utm_medium=sms","title":"Oak St","external_ref":"campaign-uuid"}

target_url must be https and its host must match SHORT_ALLOWED_HOSTS (exact or suffix, e.g. mkdrealtor.com). Same target_url + external_ref + still-active link returns 200 with the existing row instead of a new code.

Do not call the public short hostname to create links.

Environment

See .env.example and .env.prod.example. Prod secrets live in ~/Documents/secrets/url_shortening_service/ on the control node.

Generate tokens and peppers with:

python -c "import secrets; print(secrets.token_urlsafe(32))"

Never reuse DJANGO_SECRET_KEY as an API token. Never put the token in the short URL, logs, or git.

Deploy notes

Two NPM hosts, same container WEB_PORT (default 8005 prod / 8015 beta):

  1. SHORT_DOMAIN (aiml.pw / cidinn.li) — / landing + GET /[a-z0-9]{4,8}. Drop /api/, /admin/, /debug/.
  2. API hostname — proxy /api/ only. Drop /admin/. Add that Host to DJANGO_ALLOWED_HOSTS and SHORT_API_HOSTS.
S
Description
No description provided
Readme
302 KiB
Languages
Python 65.7%
CSS 21.7%
HTML 10.7%
Shell 1.3%
Dockerfile 0.6%