LAN admin on 10.0.0.128 plus quick campaign mint form #12
+5
-3
@@ -4,7 +4,7 @@
|
||||
DJANGO_ENV=dev
|
||||
DJANGO_DEBUG=true
|
||||
DJANGO_SECRET_KEY=dev-only-change-me
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,web,url-shortener
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,web,url-shortener,10.0.0.128
|
||||
|
||||
# Leave empty for SQLite when running manage.py on the host.
|
||||
# Compose ignores this and uses the bundled Postgres via COMPOSE_DATABASE_URL.
|
||||
@@ -22,8 +22,10 @@ PUBLIC_SHORT_URL=http://127.0.0.1:8005
|
||||
SHORT_PUBLIC_HOSTS=piha.lc
|
||||
# Extra Host values that also serve /api/ (localhost / docker).
|
||||
SHORT_API_HOSTS=localhost,127.0.0.1,0.0.0.0,web,url-shortener
|
||||
# Django admin — keep local. Do not add the public short hostname.
|
||||
SHORT_ADMIN_HOSTS=localhost,127.0.0.1
|
||||
# Django admin — keep off the public short hostname. LAN IP is for local compose.
|
||||
SHORT_ADMIN_HOSTS=localhost,127.0.0.1,10.0.0.128
|
||||
# Compose publish address. 0.0.0.0 so another machine can hit 10.0.0.128:8005.
|
||||
WEB_BIND=0.0.0.0
|
||||
# Named, rotatable tokens. This is what keeps /api/ closed on a public hostname.
|
||||
# Generate: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
# Format: name:secret,name:secret — never reuse DJANGO_SECRET_KEY.
|
||||
|
||||
+3
-3
@@ -77,7 +77,7 @@ Internet
|
||||
|
||||
- One NPM proxy host. `location /` → gunicorn. Django 404s `/admin/` and `/debug/`.
|
||||
- `monica_site` calls `SHORTENER_BASE_URL` (`https://piha.lc` or `https://beta.piha.li`).
|
||||
- Django admin stays on `SHORT_ADMIN_HOSTS` (localhost). Not on the public host.
|
||||
- Django admin stays on `SHORT_ADMIN_HOSTS` (localhost / LAN IP). Not on the public host.
|
||||
|
||||
### 2.4 Target allowlist
|
||||
|
||||
@@ -96,7 +96,7 @@ Even if NPM is misconfigured, the Django process must refuse the wrong surface:
|
||||
- `request.get_host()` in `SHORT_PUBLIC_HOSTS` → redirects **and** `/api/` (Bearer).
|
||||
- `request.get_host()` in `SHORT_API_HOSTS` → `/api/` (still Bearer). Extra names
|
||||
(localhost, docker) only.
|
||||
- `request.get_host()` in `SHORT_ADMIN_HOSTS` → `/admin/` (localhost only by default).
|
||||
- `request.get_host()` in `SHORT_ADMIN_HOSTS` → `/admin/` (localhost and `10.0.0.128` by default).
|
||||
- `/healthz/` allowed on both. No secrets in the body.
|
||||
|
||||
---
|
||||
@@ -355,7 +355,7 @@ Reject codes that do not match `^[a-z0-9]{4,8}$` with 404 (no extra work).
|
||||
| `PUBLIC_SHORT_URL` | origin for minted URLs, e.g. `https://piha.lc` (no trailing slash) |
|
||||
| `SHORT_PUBLIC_HOSTS` | comma list; Host values that serve redirects **and** `/api/` |
|
||||
| `SHORT_API_HOSTS` | comma list; extra Host values that serve `/api/` (localhost / docker) |
|
||||
| `SHORT_ADMIN_HOSTS` | comma list; Host values that serve `/admin/` (default localhost only) |
|
||||
| `SHORT_ADMIN_HOSTS` | comma list; Host values that serve `/admin/` (default localhost + `10.0.0.128`) |
|
||||
| `SHORTENER_API_TOKENS` | `name:secret,name:secret` — required for API |
|
||||
| `SHORT_ALLOWED_HOSTS` | allowlist for `target_url` hosts |
|
||||
| `SHORT_CODE_LENGTH` | default `6` |
|
||||
|
||||
@@ -19,8 +19,9 @@ This service is standalone. Do not fold it into `monica_site`.
|
||||
named Bearer token. No token / wrong token → **401**. No tokens configured → **503**.
|
||||
`GET /<code>` never requires a token.
|
||||
|
||||
`/admin/` is 404 on the public host (localhost only). `GET /debug/` is a mint form
|
||||
when `DEBUG=true` and never on the public short host.
|
||||
`/admin/` is 404 on the public host. Local compose serves it on `localhost` and
|
||||
`10.0.0.128` (`http://10.0.0.128:8005/admin/` from another machine on the LAN).
|
||||
`GET /debug/` is a mint form when `DEBUG=true` and never on the public short host.
|
||||
|
||||
## Local run
|
||||
|
||||
@@ -40,6 +41,7 @@ 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)
|
||||
# LAN: http://10.0.0.128:8005/admin/ (compose publishes 0.0.0.0:8005)
|
||||
```
|
||||
|
||||
Tests (SQLite, no network):
|
||||
|
||||
+3
-2
@@ -17,19 +17,20 @@ services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "127.0.0.1:8005:8000"
|
||||
- "${WEB_BIND:-0.0.0.0}:8005:8000"
|
||||
volumes:
|
||||
- ./site:/app/site
|
||||
environment:
|
||||
DJANGO_ENV: ${DJANGO_ENV:-dev}
|
||||
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-dev-only-change-me}
|
||||
DJANGO_DEBUG: ${DJANGO_DEBUG:-true}
|
||||
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0,web,url-shortener}
|
||||
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0,web,url-shortener,10.0.0.128}
|
||||
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:-piha.lc}
|
||||
SHORT_API_HOSTS: ${SHORT_API_HOSTS:-localhost,127.0.0.1,0.0.0.0,web,url-shortener}
|
||||
SHORT_ADMIN_HOSTS: ${SHORT_ADMIN_HOSTS:-localhost,127.0.0.1,10.0.0.128}
|
||||
SHORTENER_API_TOKENS: ${SHORTENER_API_TOKENS:-monica:dev-only-token}
|
||||
SHORT_ALLOWED_HOSTS: ${SHORT_ALLOWED_HOSTS:-mkdrealtor.com,aimloperations.com}
|
||||
SHORT_CODE_LENGTH: ${SHORT_CODE_LENGTH:-6}
|
||||
|
||||
+54
-1
@@ -2,8 +2,15 @@ from django.contrib import admin, messages
|
||||
from django.forms import ModelForm, ValidationError as FormValidationError
|
||||
from django.utils.html import format_html
|
||||
|
||||
from links.forms import QuickMintForm, allowlisted_domain_choices
|
||||
from links.models import Click, ShortLink
|
||||
from links.services import ValidationError, generate_code, validate_target_url
|
||||
from links.services import (
|
||||
CodeCollisionError,
|
||||
ValidationError,
|
||||
create_link,
|
||||
generate_code,
|
||||
validate_target_url,
|
||||
)
|
||||
|
||||
admin.site.site_header = "URL shortener"
|
||||
admin.site.site_title = "Shortener admin"
|
||||
@@ -156,3 +163,49 @@ class ClickAdmin(admin.ModelAdmin):
|
||||
def user_agent_short(self, obj: Click) -> str:
|
||||
ua = obj.user_agent or ""
|
||||
return (ua[:48] + "…") if len(ua) > 48 else (ua or "—")
|
||||
|
||||
|
||||
def _quick_mint_context(request, extra_context=None):
|
||||
extra = extra_context.copy() if extra_context else {}
|
||||
extra.setdefault("quick_mint_form", QuickMintForm())
|
||||
extra.setdefault("created_link", None)
|
||||
extra.setdefault("allowed_domains", allowlisted_domain_choices())
|
||||
return extra
|
||||
|
||||
|
||||
def _mint_from_form(request, form: QuickMintForm):
|
||||
token_name = request.user.get_username() if request.user.is_authenticated else "admin"
|
||||
campaign = form.cleaned_data["campaign"].strip()
|
||||
return create_link(
|
||||
target_url=form.cleaned_data["target_url"],
|
||||
title=campaign,
|
||||
external_ref="",
|
||||
expires_at=None,
|
||||
token_name=token_name or "admin",
|
||||
)
|
||||
|
||||
|
||||
_orig_index = admin.site.index
|
||||
|
||||
|
||||
def _admin_index(request, extra_context=None):
|
||||
extra = _quick_mint_context(request, extra_context)
|
||||
form = QuickMintForm(request.POST or None)
|
||||
extra["quick_mint_form"] = form
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
try:
|
||||
link, minted = _mint_from_form(request, form)
|
||||
except CodeCollisionError:
|
||||
form.add_error(None, "Could not allocate a unique code.")
|
||||
else:
|
||||
extra["created_link"] = link
|
||||
extra["quick_mint_form"] = QuickMintForm()
|
||||
messages.success(
|
||||
request,
|
||||
"Short link created." if minted else "Existing active link returned.",
|
||||
)
|
||||
return _orig_index(request, extra)
|
||||
|
||||
|
||||
admin.site.index = _admin_index
|
||||
|
||||
+56
-1
@@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from links.services import ValidationError, validate_target_url
|
||||
from links.services import ValidationError, build_tracked_url, validate_target_url
|
||||
|
||||
|
||||
class DebugCreateForm(forms.Form):
|
||||
@@ -19,3 +20,57 @@ class DebugCreateForm(forms.Form):
|
||||
return validate_target_url(raw)
|
||||
except ValidationError as exc:
|
||||
raise forms.ValidationError(str(exc)) from exc
|
||||
|
||||
|
||||
def allowlisted_domain_choices() -> list[str]:
|
||||
hosts = list(getattr(settings, "SHORT_ALLOWED_HOSTS", []) or [])
|
||||
return [host for host in hosts if host and not host.startswith("*")]
|
||||
|
||||
|
||||
class QuickMintForm(forms.Form):
|
||||
domain = forms.CharField(
|
||||
label="Domain",
|
||||
max_length=253,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"placeholder": "mkdrealtor.com",
|
||||
"list": "allowed-domains",
|
||||
"autocomplete": "off",
|
||||
}
|
||||
),
|
||||
)
|
||||
campaign = forms.CharField(
|
||||
label="Campaign",
|
||||
max_length=200,
|
||||
widget=forms.TextInput(attrs={"placeholder": "open-house"}),
|
||||
)
|
||||
source = forms.CharField(
|
||||
label="Source",
|
||||
max_length=200,
|
||||
widget=forms.TextInput(attrs={"placeholder": "sms"}),
|
||||
)
|
||||
metric = forms.CharField(
|
||||
label="Metric",
|
||||
max_length=200,
|
||||
widget=forms.TextInput(attrs={"placeholder": "listing-click"}),
|
||||
help_text="Stored as utm_medium.",
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
domain = cleaned.get("domain")
|
||||
campaign = cleaned.get("campaign")
|
||||
source = cleaned.get("source")
|
||||
metric = cleaned.get("metric")
|
||||
if not all((domain, campaign, source, metric)):
|
||||
return cleaned
|
||||
try:
|
||||
cleaned["target_url"] = build_tracked_url(
|
||||
domain=domain,
|
||||
campaign=campaign,
|
||||
source=source,
|
||||
metric=metric,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
self.add_error("domain", str(exc))
|
||||
return cleaned
|
||||
|
||||
+35
-1
@@ -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):
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n static admin_filters %}
|
||||
|
||||
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/dashboard.css" %}" {% csp_nonce_attr %}>{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
<script {% csp_nonce_attr %}>
|
||||
function copyShortUrl() {
|
||||
var el = document.getElementById("id_short_url_result");
|
||||
var btn = document.getElementById("copy-short-url");
|
||||
if (!el) return;
|
||||
var done = function () {
|
||||
if (btn) {
|
||||
btn.textContent = "Copied";
|
||||
setTimeout(function () { btn.textContent = "Copy"; }, 1500);
|
||||
}
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(el.value).then(done, function () {
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
done();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block coltype %}colMS{% endblock %}
|
||||
|
||||
{% block bodyclass %}{{ block.super }} dashboard{% endblock %}
|
||||
|
||||
{% block nav-breadcrumbs %}{% endblock %}
|
||||
|
||||
{% block nav-sidebar %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main" class="app-list">
|
||||
<div class="module" id="quick-mint-module">
|
||||
<h2>Create a short link</h2>
|
||||
{% if created_link %}
|
||||
<div class="form-row" style="padding: 12px 16px 0;">
|
||||
<label for="id_short_url_result">Short URL</label>
|
||||
<input id="id_short_url_result" type="text" readonly value="{{ created_link.public_short_url }}" size="48" style="max-width: 28rem;">
|
||||
<button type="button" class="default" id="copy-short-url" onclick="copyShortUrl()">Copy</button>
|
||||
<p class="help">Target: {{ created_link.target_url }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'admin:index' %}" style="padding: 8px 16px 16px;">
|
||||
{% csrf_token %}
|
||||
{{ quick_mint_form.non_field_errors }}
|
||||
<datalist id="allowed-domains">
|
||||
{% for host in allowed_domains %}<option value="{{ host }}">{% endfor %}
|
||||
</datalist>
|
||||
{% for field in quick_mint_form %}
|
||||
<div class="form-row">
|
||||
{{ field.errors }}
|
||||
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}<p class="help">{{ field.help_text }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="submit-row">
|
||||
<input type="submit" class="default" value="Save">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<div id="content-related">
|
||||
<div class="module" id="recent-actions-module">
|
||||
<h2>{% translate 'Recent actions' %}</h2>
|
||||
<h3>{% translate 'My actions' %}</h3>
|
||||
{% load log %}
|
||||
{% get_admin_log 10 as admin_log for_user user %}
|
||||
{% if not admin_log %}
|
||||
<p>{% translate 'None available' %}</p>
|
||||
{% else %}
|
||||
<ul class="actionlist">
|
||||
{% for entry in admin_log %}
|
||||
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
|
||||
<span class="visually-hidden">{% if entry.is_addition %}{% translate 'Added:' %}{% elif entry.is_change %}{% translate 'Changed:' %}{% elif entry.is_deletion %}{% translate 'Deleted:' %}{% endif %}</span>
|
||||
{% if entry.is_deletion or not entry.get_admin_url %}
|
||||
{{ entry.object_repr|to_object_display_value }}
|
||||
{% else %}
|
||||
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr|to_object_display_value }}</a>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if entry.content_type %}
|
||||
<span class="mini quiet">{% filter capfirst %}{{ entry.content_type.name }}{% endfilter %}</span>
|
||||
{% else %}
|
||||
<span class="mini quiet">{% translate 'Unknown content' %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+79
-1
@@ -426,7 +426,11 @@ class DebugCreateTests(TestCase):
|
||||
self.assertContains(response, link.public_short_url)
|
||||
|
||||
|
||||
ADMIN_SETTINGS = {**SETTINGS, "SHORT_ADMIN_HOSTS": ["testserver", "localhost"]}
|
||||
ADMIN_SETTINGS = {
|
||||
**SETTINGS,
|
||||
"SHORT_ADMIN_HOSTS": ["testserver", "localhost", "10.0.0.128"],
|
||||
"ALLOWED_HOSTS": [*SETTINGS["ALLOWED_HOSTS"], "10.0.0.128"],
|
||||
}
|
||||
|
||||
|
||||
@override_settings(**ADMIN_SETTINGS)
|
||||
@@ -497,3 +501,77 @@ class AdminTests(TestCase):
|
||||
self.assertEqual(response.status_code, 404)
|
||||
response = self.client.get("/admin/", HTTP_HOST="shortener.example.com")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_admin_200_on_lan_ip(self):
|
||||
response = self.client.get("/admin/", HTTP_HOST="10.0.0.128")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Create a short link")
|
||||
self.assertContains(response, "Domain")
|
||||
self.assertContains(response, "Campaign")
|
||||
self.assertContains(response, "Source")
|
||||
self.assertContains(response, "Metric")
|
||||
|
||||
def test_quick_mint_creates_tracked_url(self):
|
||||
response = self.client.post(
|
||||
"/admin/",
|
||||
{
|
||||
"domain": "mkdrealtor.com",
|
||||
"campaign": "open-house",
|
||||
"source": "sms",
|
||||
"metric": "listing-click",
|
||||
},
|
||||
HTTP_HOST="10.0.0.128",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
created = ShortLink.objects.exclude(code="a3k9xm").get()
|
||||
self.assertEqual(
|
||||
created.target_url,
|
||||
"https://mkdrealtor.com/?utm_campaign=open-house&utm_source=sms&utm_medium=listing-click",
|
||||
)
|
||||
self.assertEqual(created.title, "open-house")
|
||||
self.assertEqual(created.created_by_token, "admin")
|
||||
self.assertContains(response, created.public_short_url)
|
||||
self.assertContains(response, "Copy")
|
||||
|
||||
def test_quick_mint_rejects_unknown_host(self):
|
||||
response = self.client.post(
|
||||
"/admin/",
|
||||
{
|
||||
"domain": "evil.example",
|
||||
"campaign": "spam",
|
||||
"source": "sms",
|
||||
"metric": "click",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(ShortLink.objects.exclude(code="a3k9xm").count(), 0)
|
||||
self.assertContains(response, "host not allowlisted")
|
||||
|
||||
|
||||
class TrackedUrlTests(TestCase):
|
||||
@override_settings(**SETTINGS)
|
||||
def test_build_tracked_url(self):
|
||||
from links.services import build_tracked_url
|
||||
|
||||
url = build_tracked_url(
|
||||
domain="https://mkdrealtor.com/ignored",
|
||||
campaign="open house",
|
||||
source="sms",
|
||||
metric="listing-click",
|
||||
)
|
||||
self.assertEqual(
|
||||
url,
|
||||
"https://mkdrealtor.com/?utm_campaign=open+house&utm_source=sms&utm_medium=listing-click",
|
||||
)
|
||||
|
||||
@override_settings(**SETTINGS)
|
||||
def test_build_tracked_url_rejects_unknown_host(self):
|
||||
from links.services import ValidationError, build_tracked_url
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
build_tracked_url(
|
||||
domain="evil.example",
|
||||
campaign="c",
|
||||
source="s",
|
||||
metric="m",
|
||||
)
|
||||
|
||||
@@ -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,piha.lc,beta.piha.li",
|
||||
"localhost,127.0.0.1,0.0.0.0,testserver,web,url-shortener,10.0.0.128,piha.lc,beta.piha.li",
|
||||
)
|
||||
ALLOWED_HOSTS = allowed_hosts if allowed_hosts else ["*"]
|
||||
|
||||
@@ -185,7 +185,9 @@ SHORT_API_HOSTS = env_list(
|
||||
"localhost,127.0.0.1,0.0.0.0,testserver,web,url-shortener",
|
||||
)
|
||||
# Django admin — local/dev only. Never put the public short hostname here.
|
||||
SHORT_ADMIN_HOSTS = env_list("SHORT_ADMIN_HOSTS", "localhost,127.0.0.1")
|
||||
SHORT_ADMIN_HOSTS = env_list(
|
||||
"SHORT_ADMIN_HOSTS", "localhost,127.0.0.1,10.0.0.128"
|
||||
)
|
||||
SHORTENER_API_TOKENS = parse_api_tokens(env("SHORTENER_API_TOKENS", "") or "")
|
||||
SHORT_ALLOWED_HOSTS = env_list(
|
||||
"SHORT_ALLOWED_HOSTS", "mkdrealtor.com,aimloperations.com"
|
||||
|
||||
Reference in New Issue
Block a user