Restyle landing to match aimloperations.com and use piha.lc.
CI / test (pull_request) Successful in 4s

Public short hosts are piha.lc (prod) and beta.piha.li (beta). The root
page is a marketing CTA to contact AI ML Operations for access.
Closes #3.
This commit is contained in:
2026-08-30 08:34:10 -05:00
parent 630b770c12
commit bb1aa449d6
18 changed files with 1373 additions and 214 deletions
+2 -1
View File
@@ -13,12 +13,13 @@ DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,web,url-shortener
SITE_NAME=URL Shortening Service
CREDIT_NAME=AI ML Operations
CREDIT_URL=https://aimloperations.com
CONTACT_URL=https://aimloperations.com/contact
SHORT_DOMAIN=localhost:8005
# Origin printed in minted short_url (phones hit this). Local: this machine.
PUBLIC_SHORT_URL=http://127.0.0.1:8005
# Host values that only serve GET /<code> (no /api/).
SHORT_PUBLIC_HOSTS=go.mkdrealtor.com
SHORT_PUBLIC_HOSTS=piha.lc
# Host values that serve /api/ (Bearer required). May include a public DNS name.
SHORT_API_HOSTS=localhost,127.0.0.1,0.0.0.0,web,url-shortener
# Django admin — keep local. Do not add the public API hostname.
+10 -9
View File
@@ -13,7 +13,7 @@ DJANGO_ENV=prod
DJANGO_DEBUG=false
DJANGO_SECRET_KEY=replace-with-a-long-random-secret
# Public short host AND public API hostname (plus docker names if used).
DJANGO_ALLOWED_HOSTS=go.mkdrealtor.com,shortener.aimloperations.com,url-shortener,web
DJANGO_ALLOWED_HOSTS=piha.lc,shortener.aimloperations.com,url-shortener,web
# Shared external Postgres
DATABASE_URL=postgres://westfarn:replace-db-password@10.0.0.230:5432/url_shortener
@@ -21,18 +21,19 @@ DATABASE_URL=postgres://westfarn:replace-db-password@10.0.0.230:5432/url_shorten
# Host port (must match server-infra). NPM proxies SHORT_DOMAIN here.
WEB_PORT=8005
SHORT_DOMAIN=go.mkdrealtor.com
PUBLIC_SHORT_URL=https://go.mkdrealtor.com
SHORT_PUBLIC_HOSTS=go.mkdrealtor.com
SHORT_DOMAIN=piha.lc
PUBLIC_SHORT_URL=https://piha.lc
SHORT_PUBLIC_HOSTS=piha.lc
SHORT_API_HOSTS=shortener.aimloperations.com,url-shortener,web
SHORT_ADMIN_HOSTS=localhost,127.0.0.1
# Generate: python -c "import secrets; print(secrets.token_urlsafe(32))"
# This token is the only thing that authorizes minting. Treat it as a secret.
# monica_site sends: Authorization: Bearer monica:<same-secret>
SHORTENER_API_TOKENS=monica:replace-with-token-urlsafe-32
SHORT_ALLOWED_HOSTS=mkdrealtor.com
SHORT_ALLOWED_HOSTS=mkdrealtor.com,aimloperations.com
SHORT_CODE_LENGTH=6
CLICK_IP_PEPPER=replace-with-a-distinct-pepper
CONTACT_URL=https://aimloperations.com/contact
GUNICORN_WORKERS=2
GUNICORN_BIND=0.0.0.0:8000
@@ -44,11 +45,11 @@ GUNICORN_BIND=0.0.0.0:8000
# DJANGO_ENV=beta
# DJANGO_DEBUG=false
# DJANGO_SECRET_KEY=replace-with-a-different-beta-secret
# DJANGO_ALLOWED_HOSTS=go-beta.example.com,shortener-beta.aimloperations.com,url-shortener,web
# DJANGO_ALLOWED_HOSTS=beta.piha.li,shortener-beta.aimloperations.com,url-shortener,web
# DATABASE_URL=postgres://westfarn:replace-db-password@10.0.0.230:5432/url_shortener_beta
# WEB_PORT=8015
# SHORT_DOMAIN=go-beta.example.com
# PUBLIC_SHORT_URL=https://go-beta.example.com
# SHORT_PUBLIC_HOSTS=go-beta.example.com
# SHORT_DOMAIN=beta.piha.li
# PUBLIC_SHORT_URL=https://beta.piha.li
# SHORT_PUBLIC_HOSTS=beta.piha.li
# SHORTENER_API_TOKENS=monica:replace-with-a-different-token
# CLICK_IP_PEPPER=replace-with-a-different-pepper
+2 -2
View File
@@ -26,8 +26,8 @@ jobs:
DATABASE_URL: ""
DB_HOST: ""
SHORTENER_API_TOKENS: monica:dev-only-token
PUBLIC_SHORT_URL: https://go.mkdrealtor.com
SHORT_PUBLIC_HOSTS: go.mkdrealtor.com
PUBLIC_SHORT_URL: https://piha.lc
SHORT_PUBLIC_HOSTS: piha.lc
SHORT_API_HOSTS: testserver,localhost,127.0.0.1
SHORT_ALLOWED_HOSTS: mkdrealtor.com
CLICK_IP_PEPPER: test-pepper
+4 -4
View File
@@ -26,8 +26,8 @@ jobs:
DATABASE_URL: ""
DB_HOST: ""
SHORTENER_API_TOKENS: monica:dev-only-token
PUBLIC_SHORT_URL: https://go.mkdrealtor.com
SHORT_PUBLIC_HOSTS: go.mkdrealtor.com
PUBLIC_SHORT_URL: https://piha.lc
SHORT_PUBLIC_HOSTS: piha.lc
SHORT_API_HOSTS: testserver,localhost,127.0.0.1
SHORT_ALLOWED_HOSTS: mkdrealtor.com
CLICK_IP_PEPPER: test-pepper
@@ -63,8 +63,8 @@ jobs:
-e DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,testserver \
-e DATABASE_URL=postgres://url_shortener:url_shortener@db:5432/url_shortener \
-e SHORTENER_API_TOKENS=monica:dev-only-token \
-e PUBLIC_SHORT_URL=https://go.mkdrealtor.com \
-e SHORT_PUBLIC_HOSTS=go.mkdrealtor.com \
-e PUBLIC_SHORT_URL=https://piha.lc \
-e SHORT_PUBLIC_HOSTS=piha.lc \
-e SHORT_API_HOSTS=testserver,localhost,127.0.0.1 \
-e SHORT_ALLOWED_HOSTS=mkdrealtor.com \
-e CLICK_IP_PEPPER=test-pepper \
+4 -4
View File
@@ -24,8 +24,8 @@ jobs:
DATABASE_URL: ""
DB_HOST: ""
SHORTENER_API_TOKENS: monica:dev-only-token
PUBLIC_SHORT_URL: https://go.mkdrealtor.com
SHORT_PUBLIC_HOSTS: go.mkdrealtor.com
PUBLIC_SHORT_URL: https://piha.lc
SHORT_PUBLIC_HOSTS: piha.lc
SHORT_API_HOSTS: testserver,localhost,127.0.0.1
SHORT_ALLOWED_HOSTS: mkdrealtor.com
CLICK_IP_PEPPER: test-pepper
@@ -61,8 +61,8 @@ jobs:
-e DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,testserver \
-e DATABASE_URL=postgres://url_shortener:url_shortener@db:5432/url_shortener \
-e SHORTENER_API_TOKENS=monica:dev-only-token \
-e PUBLIC_SHORT_URL=https://go.mkdrealtor.com \
-e SHORT_PUBLIC_HOSTS=go.mkdrealtor.com \
-e PUBLIC_SHORT_URL=https://piha.lc \
-e SHORT_PUBLIC_HOSTS=piha.lc \
-e SHORT_API_HOSTS=testserver,localhost,127.0.0.1 \
-e SHORT_ALLOWED_HOSTS=mkdrealtor.com \
-e CLICK_IP_PEPPER=test-pepper \
+3 -3
View File
@@ -3,13 +3,13 @@
For other services (`monica_site`, later callers) that mint short links.
This service has two public hostnames. **Call the API host.** Never create links
on the short domain (`aiml.pw` / `cidinn.li` / `go.mkdrealtor.com`). That host
on the short domain (`piha.lc` / `beta.piha.li`). That host
only serves `GET /` (landing) and `GET /<code>` (302). `/api/` there is **404**.
| Host | Example | What you call |
|------|---------|----------------|
| API | `https://shortener.aimloperations.com` | `POST/GET /api/links/` |
| Short | `https://aiml.pw` (or `cidinn.li`) | phones only — `GET /<code>` |
| Short | `https://piha.lc` (beta: `https://beta.piha.li`) | phones only — `GET /<code>` |
Local compose: API + redirects on `http://127.0.0.1:8005`.
@@ -147,7 +147,7 @@ Content-Type: application/json
```json
{
"code": "a3k9xm",
"short_url": "https://aiml.pw/a3k9xm",
"short_url": "https://piha.lc/a3k9xm",
"target_url": "https://mkdrealtor.com/listings/oak-st?utm_source=monica&utm_medium=sms",
"title": "Oak St listing",
"is_active": true,
+6 -6
View File
@@ -2,7 +2,7 @@
Django 6 service in Docker, packaged with `uv`. First caller is `monica_site` (SMS campaign
bodies and ad-hoc texts). A short public domain will be pointed at this service later;
until then a subdomain (for example `go.mkdrealtor.com`) is enough.
until then a subdomain is enough. Public short hosts: `piha.lc` (prod), `beta.piha.li` (beta).
This document is the build spec. Implement in this repo. Do not fold the shortener into
`monica_site`.
@@ -69,7 +69,7 @@ cannot mint links without a configured Bearer token. Empty `SHORTENER_API_TOKENS
```
Internet
├─ NPM: SHORT_DOMAIN (mkd.to / go.mkdrealtor.com)
├─ NPM: SHORT_DOMAIN (piha.lc / beta.piha.li)
│ location ~*^/[a-z0-9]{4,8}$ → gunicorn (redirects only)
│ /api/* → 404 / drop
│ /admin/ → 404 / drop
@@ -130,7 +130,7 @@ Caller (`monica_site`) is an HTTP client. It does not share this database.
Until the purchased short domain exists:
- Set `SHORT_DOMAIN=go.mkdrealtor.com` (or `localhost:8005` locally).
- Set `SHORT_DOMAIN=piha.lc` (or `localhost:8005` locally).
- Codes do not change when DNS is swapped. Only `SHORT_DOMAIN` / `PUBLIC_SHORT_URL` change.
---
@@ -274,7 +274,7 @@ Response `201`:
```json
{
"code": "a3k9xm",
"short_url": "https://go.mkdrealtor.com/a3k9xm",
"short_url": "https://piha.lc/a3k9xm",
"target_url": "https://mkdrealtor.com/listings/oak-st?utm_source=monica&utm_medium=sms",
"title": "Oak St listing",
"is_active": true,
@@ -355,8 +355,8 @@ Reject codes that do not match `^[a-z0-9]{4,8}$` with 404 (no extra work).
| `DJANGO_DEBUG` | false in prod |
| `DJANGO_ALLOWED_HOSTS` | public short host **and** API hostname |
| `DATABASE_URL` | Postgres; empty → SQLite |
| `SHORT_DOMAIN` | public hostname phones use (`go.mkdrealtor.com` then `mkd.to`) |
| `PUBLIC_SHORT_URL` | origin for minted URLs, e.g. `https://go.mkdrealtor.com` (no trailing slash) |
| `SHORT_DOMAIN` | public hostname phones use (`piha.lc` prod, `beta.piha.li` beta) |
| `PUBLIC_SHORT_URL` | origin for minted URLs, e.g. `https://piha.lc` (no trailing slash) |
| `SHORT_PUBLIC_HOSTS` | comma list; Host values that only serve redirects |
| `SHORT_API_HOSTS` | comma list; Host values that serve `/api/` (public API DNS and/or docker name) |
| `SHORT_ADMIN_HOSTS` | comma list; Host values that serve `/admin/` (default localhost only) |
+1 -1
View File
@@ -125,7 +125,7 @@ short URL, logs, or git.
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}`.
1. `SHORT_DOMAIN` (`piha.lc` prod / `beta.piha.li` beta) — `/` 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`.
+1 -1
View File
@@ -28,7 +28,7 @@ services:
DATABASE_URL: ${COMPOSE_DATABASE_URL:-postgres://url_shortener:url_shortener@db:5432/url_shortener}
SHORT_DOMAIN: ${SHORT_DOMAIN:-localhost:8005}
PUBLIC_SHORT_URL: ${PUBLIC_SHORT_URL:-http://127.0.0.1:8005}
SHORT_PUBLIC_HOSTS: ${SHORT_PUBLIC_HOSTS:-go.mkdrealtor.com}
SHORT_PUBLIC_HOSTS: ${SHORT_PUBLIC_HOSTS:-piha.lc}
SHORT_API_HOSTS: ${SHORT_API_HOSTS:-localhost,127.0.0.1,0.0.0.0,web,url-shortener}
SHORTENER_API_TOKENS: ${SHORTENER_API_TOKENS:-monica:dev-only-token}
SHORT_ALLOWED_HOSTS: ${SHORT_ALLOWED_HOSTS:-mkdrealtor.com,aimloperations.com}
+1
View File
@@ -10,5 +10,6 @@ def branding(request):
"PUBLIC_SHORT_URL": settings.PUBLIC_SHORT_URL,
"CREDIT_NAME": settings.CREDIT_NAME,
"CREDIT_URL": settings.CREDIT_URL,
"CONTACT_URL": settings.CONTACT_URL,
"show_debug_create": settings.DEBUG and not is_public_host(request.get_host()),
}
+69
View File
@@ -0,0 +1,69 @@
/* Shortener-only bits on top of company_site style.css */
.hero-cta {
margin-top: 1.75rem;
}
.hero-subtitle--wide {
max-width: 40rem;
}
.debug-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1100;
background: var(--secondary-color);
color: #fff;
text-align: center;
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.4rem 1rem;
}
.debug-banner a {
color: #fff;
text-decoration: underline;
}
body.has-debug-banner nav {
top: 1.7rem;
}
.page-main {
min-height: 60vh;
}
.debug-section {
padding-top: 8rem;
}
.debug-section .form-control {
margin-top: 0.35rem;
}
.debug-section label {
display: block;
margin-bottom: 1rem;
color: var(--text-muted);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.debug-result {
margin-bottom: 1.5rem;
}
.debug-result code {
color: var(--primary-color);
word-break: break-all;
}
.brand-logo-img {
height: 28px;
width: auto;
display: block;
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+57 -118
View File
@@ -1,128 +1,67 @@
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ SITE_NAME }}{% endblock %}</title>
<style>
:root {
--ink: #1c1914;
--paper: #f3ead8;
--rule: #c9b896;
--pine: #2a4a3c;
--rust: #9a3f24;
--muted: #6b6254;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body {
background: var(--paper);
color: var(--ink);
font-family: "Iowan Old Style", Palatino, "Palatino Linotype", "Times New Roman", serif;
line-height: 1.5;
}
.shell {
max-width: 40rem;
margin: 0 auto;
padding: 3.5rem 1.5rem 2rem;
}
.eyebrow {
font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace;
font-size: 0.72rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--pine);
margin: 0 0 0.75rem;
}
h1 {
font-size: clamp(2.4rem, 8vw, 4.2rem);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 0.95;
margin: 0 0 1.25rem;
}
p { margin: 0 0 1rem; font-size: 1.15rem; }
.lede { color: var(--muted); max-width: 32rem; }
a { color: var(--rust); }
a:hover { color: var(--ink); }
footer {
margin-top: 3.5rem;
padding-top: 1rem;
border-top: 1px solid var(--rule);
font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace;
font-size: 0.78rem;
color: var(--muted);
}
footer a { color: inherit; }
.banner {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
background: var(--ink);
color: var(--paper);
padding: 0.4rem 1.5rem;
}
form { margin-top: 1.75rem; display: grid; gap: 0.9rem; }
label {
display: grid;
gap: 0.3rem;
font-family: ui-monospace, Menlo, monospace;
font-size: 0.78rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--pine);
}
input[type="url"], input[type="text"] {
font: 1rem/1.4 "Iowan Old Style", Palatino, serif;
padding: 0.55rem 0.65rem;
border: 1px solid var(--rule);
background: #fffdf6;
color: var(--ink);
}
button, .btn {
appearance: none;
border: 0;
background: var(--pine);
color: var(--paper);
font-family: ui-monospace, Menlo, monospace;
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.7rem 1rem;
cursor: pointer;
text-decoration: none;
display: inline-block;
width: fit-content;
}
button:hover, .btn:hover { background: var(--ink); }
.error { color: var(--rust); font-size: 0.95rem; }
.result {
margin-top: 1.5rem;
padding: 1rem;
border: 1px dashed var(--pine);
background: #fffdf6;
}
.result code {
font-size: 1.05rem;
word-break: break-all;
}
.messages { list-style: none; padding: 0; margin: 0 0 1rem; color: var(--pine); }
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ SITE_NAME }} · {{ CREDIT_NAME }}{% endblock %}</title>
<meta name="description" content="{% block meta_description %}{{ SITE_NAME }} by {{ CREDIT_NAME }}. Short links for campaigns and trusted callers. Request access at aimloperations.com.{% endblock %}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<link href="{% static 'core/css/style.css' %}" rel="stylesheet">
<link href="{% static 'core/css/overrides.css' %}" rel="stylesheet">
<link rel="icon" type="image/png" href="{% static 'core/img/logo.png' %}">
</head>
<body>
<body{% if debug %} class="has-debug-banner"{% endif %}>
{% if debug %}
<div class="banner">Debug — create page is local only</div>
<div class="debug-banner">Debug — mint form is local only{% if show_debug_create %} · <a href="{% url 'debug-create' %}">Create a short link</a>{% endif %}</div>
{% endif %}
<main class="shell">
<nav aria-label="Main navigation">
<a href="{{ CREDIT_URL }}" class="brand-logo">
<img src="{% static 'core/img/logo.png' %}" alt="{{ CREDIT_NAME }}" class="brand-logo-img" width="243" height="28">
</a>
<button type="button" class="mobile-menu-btn" aria-label="Open menu" aria-expanded="false" aria-controls="main-nav-links"></button>
<ul class="nav-links" id="main-nav-links">
<li><a href="{{ CREDIT_URL }}">Home</a></li>
<li><a href="{{ CREDIT_URL }}/web_design">Web Design</a></li>
<li><a href="{{ CONTACT_URL }}" class="btn" style="border: 1px solid var(--primary-color); padding: 0.4rem 1rem; border-radius: 4px; margin-left: 1rem; color: #000;">Contact</a></li>
</ul>
</nav>
<main class="page-main">
{% block content %}{% endblock %}
<footer>
Created by
<a href="{{ CREDIT_URL }}">{{ CREDIT_NAME }}</a>
{% if show_debug_create %}
· <a href="{% url 'debug-create' %}">Create a short link</a>
{% endif %}
</footer>
</main>
<footer class="footer">
<div class="container">
<p class="footer-text">{{ CREDIT_NAME }}, LLC — Forward-deployed AI engineering.</p>
<p class="footer-links">
<a href="{{ CREDIT_URL }}/terms">Terms &amp; Privacy</a>
{% if show_debug_create %}
<span class="footer-separator">|</span>
<a href="{% url 'debug-create' %}">Create a short link</a>
{% endif %}
</p>
<p class="footer-text footer-text--muted">&copy; 2023 -
<script>document.write(new Date().getFullYear());</script> All rights reserved.
</p>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function () {
const mobileBtn = document.querySelector('.mobile-menu-btn');
const navLinks = document.querySelector('.nav-links');
if (mobileBtn && navLinks) {
mobileBtn.addEventListener('click', function () {
const isOpen = navLinks.classList.toggle('active');
mobileBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
mobileBtn.setAttribute('aria-label', isOpen ? 'Close menu' : 'Open menu');
});
}
});
</script>
</body>
</html>
+54 -11
View File
@@ -1,14 +1,57 @@
{% extends "core/base.html" %}
{% block title %}{{ SITE_NAME }}{% endblock %}
{% block title %}{{ SHORT_DOMAIN }} · URL shortener · {{ CREDIT_NAME }}{% endblock %}
{% block content %}
<p class="eyebrow">{{ SITE_NAME }}</p>
<h1>{{ SHORT_DOMAIN }}</h1>
<p class="lede">
This is a URL shortening service. A path like
<strong>/a3k9xm</strong> sends you to the long HTTPS URL.
</p>
<p class="lede">
Created by <a href="{{ CREDIT_URL }}">{{ CREDIT_NAME }}</a>
· <a href="{{ CREDIT_URL }}">aimloperations.com</a>
</p>
<div class="hero-section">
<div class="hero-content">
<h1 class="hero-title">{{ SHORT_DOMAIN }}</h1>
<p class="hero-subtitle hero-subtitle--wide">
Short links for campaigns and trusted callers. Built by
{{ CREDIT_NAME }} — not an open public shortener.
</p>
<div class="hero-cta">
<a href="{{ CONTACT_URL }}" class="btn">Request access</a>
</div>
</div>
</div>
<div class="section" style="background: var(--surface-color);">
<div class="container">
<h2 class="section-title">A shortener you can hand to SMS</h2>
<p style="text-align: center; max-width: 820px; margin: 0 auto; color: var(--text-muted); font-size: 1.1rem;">
{{ SHORT_DOMAIN }} turns a long HTTPS URL into a short path like
<strong>/a3k9xm</strong>. Recipients tap it and get a 302 to your site.
Creating links is Bearer-authenticated — only onboarded services can mint.
</p>
</div>
</div>
<div class="section">
<div class="container">
<h2 class="section-title">Want access?</h2>
<div class="card-grid">
<div class="card">
<h3 class="card-title">For your product</h3>
<p class="card-text">
We issue a named token and allowlist your destination hosts.
Your app POSTs to the API and drops <code>short_url</code> into SMS or email.
</p>
</div>
<div class="card">
<h3 class="card-title">Not self-serve</h3>
<p class="card-text">
There is no public signup. If you want this for a campaign or another
{{ CREDIT_NAME }} property, talk to us.
</p>
</div>
<div class="card">
<h3 class="card-title">Talk to us</h3>
<p class="card-text">
Contact <a href="{{ CREDIT_URL }}">aimloperations.com</a> and tell us
which hosts you need to shorten to.
</p>
<a href="{{ CONTACT_URL }}" class="btn" style="margin-top: 1rem; padding: 0.7rem 1.4rem;">Contact</a>
</div>
</div>
</div>
</div>
{% endblock %}
+36 -28
View File
@@ -1,35 +1,43 @@
{% extends "core/base.html" %}
{% block title %}Create short link · debug{% endblock %}
{% block content %}
<p class="eyebrow">Debug only</p>
<h1>Create a short link</h1>
<p class="lede">Uses the same allowlist as the API. Not served when <code>DEBUG</code> is false.</p>
<div class="section debug-section">
<div class="container" style="max-width: 40rem;">
<h1 class="hero-title" style="font-size: 2.2rem;">Create a short link</h1>
<p class="hero-subtitle" style="margin: 1rem 0 2rem; text-align: left;">
DEBUG only. Same allowlist as the API. Not served when <code>DEBUG</code> is false.
</p>
{% if messages %}
<ul class="messages">
{% for message in messages %}<li>{{ message }}</li>{% endfor %}
</ul>
{% endif %}
{% if messages %}
<ul class="messages" style="list-style: none; padding: 0; margin: 0 0 1rem; color: var(--primary-color);">
{% for message in messages %}<li>{{ message }}</li>{% endfor %}
</ul>
{% endif %}
{% if created_link %}
<div class="result">
<p>Short URL</p>
<p><code>{{ created_link.public_short_url }}</code></p>
<p>Target: {{ created_link.target_url }}</p>
<a class="btn" href="{{ created_link.public_short_url }}">Open</a>
{% if created_link %}
<div class="card debug-result">
<p class="card-title">Short URL</p>
<p><code>{{ created_link.public_short_url }}</code></p>
<p class="card-text">Target: {{ created_link.target_url }}</p>
<a class="btn" href="{{ created_link.public_short_url }}" style="margin-top: 1rem; padding: 0.7rem 1.4rem;">Open</a>
</div>
{% endif %}
<form method="post" action="{% url 'debug-create' %}">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<label>
{{ field.label }}
<input class="form-control" type="{{ field.field.widget.input_type|default:'text' }}"
name="{{ field.name }}" value="{{ field.value|default_if_none:'' }}"
{% if field.field.widget.attrs.placeholder %}placeholder="{{ field.field.widget.attrs.placeholder }}"{% endif %}
{% if field.field.widget.attrs.autofocus %}autofocus{% endif %}>
{% if field.errors %}<span class="alert-text" style="color: #ff6b6b;">{{ field.errors|striptags }}</span>{% endif %}
</label>
{% endfor %}
<button type="submit" class="btn">Mint</button>
</form>
</div>
{% endif %}
<form method="post" action="{% url 'debug-create' %}">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<label>
{{ field.label }}
{{ field }}
{% if field.errors %}<span class="error">{{ field.errors|striptags }}</span>{% endif %}
</label>
{% endfor %}
<button type="submit">Mint</button>
</form>
</div>
{% endblock %}
+27 -24
View File
@@ -18,8 +18,9 @@ TOKENS = [("monica", "dev-only-token")]
SETTINGS = dict(
SHORTENER_API_TOKENS=TOKENS,
PUBLIC_SHORT_URL="https://go.mkdrealtor.com",
SHORT_PUBLIC_HOSTS=["go.mkdrealtor.com"],
SHORT_DOMAIN="piha.lc",
PUBLIC_SHORT_URL="https://piha.lc",
SHORT_PUBLIC_HOSTS=["piha.lc"],
SHORT_API_HOSTS=["testserver", "localhost", "127.0.0.1", "shortener.example.com"],
SHORT_ADMIN_HOSTS=["localhost", "127.0.0.1"],
SHORT_ALLOWED_HOSTS=["mkdrealtor.com"],
@@ -28,7 +29,7 @@ SETTINGS = dict(
"testserver",
"localhost",
"127.0.0.1",
"go.mkdrealtor.com",
"piha.lc",
"shortener.example.com",
],
SHORT_CODE_LENGTH=6,
@@ -93,7 +94,7 @@ class HostSplitTests(TestCase):
data=json.dumps({"target_url": "https://mkdrealtor.com/x"}),
content_type="application/json",
HTTP_AUTHORIZATION=AUTH,
HTTP_HOST="go.mkdrealtor.com",
HTTP_HOST="piha.lc",
)
self.assertEqual(response.status_code, 404)
@@ -103,7 +104,7 @@ class HostSplitTests(TestCase):
"localhost",
"127.0.0.1",
"shortener.example.com",
"go.mkdrealtor.com",
"piha.lc",
]
)
def test_short_host_never_serves_api_even_if_also_listed_as_api(self):
@@ -112,12 +113,12 @@ class HostSplitTests(TestCase):
data=json.dumps({"target_url": "https://mkdrealtor.com/x"}),
content_type="application/json",
HTTP_AUTHORIZATION=AUTH,
HTTP_HOST="go.mkdrealtor.com",
HTTP_HOST="piha.lc",
)
self.assertEqual(response.status_code, 404)
def test_public_host_admin_404(self):
response = self.client.get("/admin/", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get("/admin/", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 404)
def test_public_api_host_without_bearer_401(self):
@@ -155,7 +156,7 @@ class HostSplitTests(TestCase):
self.assertEqual(response.status_code, 404)
def test_healthz_on_public_and_api(self):
for host in ("go.mkdrealtor.com", "testserver", "shortener.example.com"):
for host in ("piha.lc", "testserver", "shortener.example.com"):
response = self.client.get("/healthz/", HTTP_HOST=host)
self.assertEqual(response.status_code, 200)
self.assertEqual(_json(response), {"status": "ok"})
@@ -216,7 +217,7 @@ class RedirectTests(TestCase):
def test_active_code_302_to_target(self):
response = self.client.get(
"/a3k9xm", HTTP_HOST="go.mkdrealtor.com", follow=False
"/a3k9xm", HTTP_HOST="piha.lc", follow=False
)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(response.status_code, 301)
@@ -226,7 +227,7 @@ class RedirectTests(TestCase):
def test_head_also_302_without_click(self):
response = self.client.head(
"/a3k9xm", HTTP_HOST="go.mkdrealtor.com", follow=False
"/a3k9xm", HTTP_HOST="piha.lc", follow=False
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
@@ -239,34 +240,36 @@ class RedirectTests(TestCase):
def test_inactive_404(self):
self.link.is_active = False
self.link.save()
response = self.client.get("/a3k9xm", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get("/a3k9xm", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 404)
def test_expired_404(self):
self.link.expires_at = timezone.now() - timedelta(minutes=1)
self.link.save()
response = self.client.get("/a3k9xm", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get("/a3k9xm", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 404)
def test_unknown_404(self):
response = self.client.get("/zzzzzz", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get("/zzzzzz", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 404)
def test_invalid_code_shape_404(self):
response = self.client.get("/AB", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get("/AB", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 404)
def test_public_root_is_landing(self):
response = self.client.get("/", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get("/", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "URL Shortening Service")
self.assertContains(response, "piha.lc")
self.assertContains(response, "Request access")
self.assertContains(response, "aimloperations.com")
self.assertContains(response, "Contact")
self.assertNotContains(response, "Create a short link")
def test_click_row_and_count(self):
self.client.get(
"/a3k9xm",
HTTP_HOST="go.mkdrealtor.com",
HTTP_HOST="piha.lc",
HTTP_USER_AGENT="sms-client",
)
self.link.refresh_from_db()
@@ -296,7 +299,7 @@ class CreateTests(TestCase):
)
self.assertEqual(response.status_code, 201)
body = _json(response)
self.assertTrue(body["short_url"].startswith("https://go.mkdrealtor.com/"))
self.assertTrue(body["short_url"].startswith("https://piha.lc/"))
self.assertNotIn("testserver", body["short_url"])
def test_idempotent_same_target_and_external_ref(self):
@@ -349,9 +352,9 @@ class CreateTests(TestCase):
self.assertEqual(first.status_code, 200)
self.assertEqual(second.status_code, 200)
self.assertFalse(_json(second)["is_active"])
self.client.get(f"/{code}", HTTP_HOST="go.mkdrealtor.com")
self.client.get(f"/{code}", HTTP_HOST="piha.lc")
# disabled → no redirect
response = self.client.get(f"/{code}", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get(f"/{code}", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 404)
@@ -379,12 +382,12 @@ class UseCaseTests(TestCase):
self.assertEqual(create.status_code, 201)
body = _json(create)
self.assertEqual(
body["short_url"], f"https://go.mkdrealtor.com/{body['code']}"
body["short_url"], f"https://piha.lc/{body['code']}"
)
follow = self.client.get(
f"/{body['code']}",
HTTP_HOST="go.mkdrealtor.com",
HTTP_HOST="piha.lc",
follow=False,
)
self.assertEqual(follow.status_code, 302)
@@ -408,7 +411,7 @@ class DebugCreateTests(TestCase):
@override_settings(DEBUG=True)
def test_hidden_on_public_short_host_even_in_debug(self):
response = self.client.get("/debug/", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get("/debug/", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 404)
@override_settings(DEBUG=True)
@@ -494,7 +497,7 @@ class AdminTests(TestCase):
def test_admin_404_on_public_hosts(self):
self.client.logout()
response = self.client.get("/admin/", HTTP_HOST="go.mkdrealtor.com")
response = self.client.get("/admin/", HTTP_HOST="piha.lc")
self.assertEqual(response.status_code, 404)
response = self.client.get("/admin/", HTTP_HOST="shortener.example.com")
self.assertEqual(response.status_code, 404)
+5 -2
View File
@@ -89,7 +89,7 @@ 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",
"localhost,127.0.0.1,0.0.0.0,testserver,web,url-shortener,piha.lc,beta.piha.li",
)
ALLOWED_HOSTS = allowed_hosts if allowed_hosts else ["*"]
@@ -176,7 +176,7 @@ 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(
PUBLIC_SHORT_URL = (env("PUBLIC_SHORT_URL", "https://piha.lc") or "").rstrip(
"/"
)
SHORT_PUBLIC_HOSTS = env_list("SHORT_PUBLIC_HOSTS", SHORT_DOMAIN.split(":")[0])
@@ -196,3 +196,6 @@ 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"
CONTACT_URL = env("CONTACT_URL", "https://aimloperations.com/contact") or (
"https://aimloperations.com/contact"
)