From a4ecd386b644255f93342009cae1c93aa8277f10 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven Date: Fri, 5 Sep 2025 22:47:22 +0200 Subject: [PATCH] Support hosting at subpath "In principle" setting `SCRIPT_NAME` is enough. The way we do this is [1] using `FORCE_SCRIPT_NAME` (which does not depend on messing with reverse proxy settings and [2] by deducing the correct value from `BASE_URL` (which must be set anyway) automatically. By works I mean: `reverse` and `{% url` pick it up from there. However, there are subtleties / extra work: * `STATIC_URL` is needed too b/c https://code.djangoproject.com/ticket/34028 * in many pre-existing code I just created a path manually in the html. Such hrefs are obviously not magically fixed for script_name. Rather than doing the "full rewrite" (into `{% url`) this commit just prepends the `script_name` in those cases. That's the way forward that will least likely break and it gives us something to grep for if we ever want to 'do it right'. * `LOGIN_REDIRECT_URL` and `LOGIN_URL` needed to use a view-name for this to work (using a view-name gets revolved using the thing that introduces `script_name`) Checked, no work needed: * views (`redirect` and `HttpResponseRedirect`) * html uses of action="..." Fix #93 --- bugsink/conf_templates/docker.py.template | 10 +++++++- .../conf_templates/singleserver.py.template | 9 ++++++- bugsink/context_processors.py | 2 ++ bugsink/middleware.py | 3 ++- bugsink/settings/default.py | 3 ++- bugsink/settings/development.py | 12 ++++++++-- bugsink/utils.py | 21 ++++++++++++++++ issues/templates/issues/base.html | 24 +++++++++---------- issues/templates/issues/event_list.html | 4 ++-- issues/templates/issues/issue_list.html | 2 +- projects/templates/projects/project_list.html | 2 +- templates/bugsink/login.html | 2 +- templates/signup.html | 2 +- theme/templates/bare_base.html | 4 ++-- theme/templates/base.html | 14 +++++------ users/templates/users/confirm_email.html | 2 +- users/templates/users/confirm_email_sent.html | 2 +- users/templates/users/logged_out.html | 2 +- .../users/request_reset_password.html | 2 +- .../templates/users/resend_confirmation.html | 2 +- users/templates/users/reset_password.html | 2 +- .../users/reset_password_email_sent.html | 2 +- 22 files changed, 88 insertions(+), 40 deletions(-) diff --git a/bugsink/conf_templates/docker.py.template b/bugsink/conf_templates/docker.py.template index b5d93db..398e607 100644 --- a/bugsink/conf_templates/docker.py.template +++ b/bugsink/conf_templates/docker.py.template @@ -1,7 +1,7 @@ import os from urllib.parse import urlparse -from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood +from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name from bugsink.settings.default import * # noqa from bugsink.settings.default import DATABASES @@ -195,3 +195,11 @@ if os.getenv("FILE_EVENT_STORAGE_PATH"): "USE_FOR_WRITE": os.getenv("FILE_EVENT_STORAGE_USE_FOR_WRITE", "false").lower() in ("true", "1", "yes"), }, } + + +FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"]) +if FORCE_SCRIPT_NAME: + # "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME + # automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028 + # so we'll just be explicit about it. + STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/" diff --git a/bugsink/conf_templates/singleserver.py.template b/bugsink/conf_templates/singleserver.py.template index 4ec6f7b..dbcef0e 100644 --- a/bugsink/conf_templates/singleserver.py.template +++ b/bugsink/conf_templates/singleserver.py.template @@ -2,7 +2,7 @@ # This is the configuration for the singleserver setup for Bugsink in production. from bugsink.settings.default import * # noqa -from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood +from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "{{ secret_key }}" @@ -126,3 +126,10 @@ ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"]) # Alternatively, you can set the ALLOWED_HOSTS manually: # ALLOWED_HOSTS = ["{{ host }}"] + +FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"]) +if FORCE_SCRIPT_NAME: + # "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME + # automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028 + # so we'll just be explicit about it. + STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/" diff --git a/bugsink/context_processors.py b/bugsink/context_processors.py index 172560e..f6a9818 100644 --- a/bugsink/context_processors.py +++ b/bugsink/context_processors.py @@ -9,6 +9,7 @@ from django.urls import reverse from django.contrib.auth.models import AnonymousUser from django.db.utils import OperationalError from django.db.models import Sum +from django.urls import get_script_prefix from bugsink.app_settings import get_settings, CB_ANYBODY from bugsink.transaction import durable_atomic @@ -136,6 +137,7 @@ def useful_settings_processor(request): 'registration_enabled': get_settings().USER_REGISTRATION == CB_ANYBODY, 'app_settings': get_settings(), 'system_warnings': get_system_warnings, + 'script_prefix': get_script_prefix().rstrip("/"), # TODO why } diff --git a/bugsink/middleware.py b/bugsink/middleware.py index 7ebe665..926d242 100644 --- a/bugsink/middleware.py +++ b/bugsink/middleware.py @@ -8,6 +8,7 @@ from django.core.exceptions import SuspiciousOperation from django.utils.translation import get_supported_language_variant from django.utils.translation.trans_real import parse_accept_lang_header from django.utils import translation +from django.urls import get_script_prefix performance_logger = logging.getLogger("bugsink.performance.views") @@ -48,7 +49,7 @@ class LoginRequiredMiddleware: # we explicitly ignore the admin and accounts paths, and the api; we can always push this to a setting later for path in ["/admin", "/accounts", "/api"]: - if request.path.startswith(path): + if request.path.startswith(get_script_prefix().rstrip("/") + path): return None if getattr(view_func, 'login_exempt', False): diff --git a/bugsink/settings/default.py b/bugsink/settings/default.py index f4881fe..0e907a5 100644 --- a/bugsink/settings/default.py +++ b/bugsink/settings/default.py @@ -221,7 +221,8 @@ DATABASE_ROUTERS = ("bugsink.dbrouters.SeparateSnappeaDBRouter",) CONN_MAX_AGE = 0 -LOGIN_REDIRECT_URL = "/" +LOGIN_REDIRECT_URL = "home" +LOGIN_URL = "login" # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators diff --git a/bugsink/settings/development.py b/bugsink/settings/development.py index 36b1702..922483b 100644 --- a/bugsink/settings/development.py +++ b/bugsink/settings/development.py @@ -6,7 +6,7 @@ import os from django.utils._os import safe_join from sentry_sdk_extensions.transport import MoreLoudlyFailingTransport -from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood +from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name # no_bandit_expl: _development_ settings, we know that this is insecure; would fail to deploy in prod if (as configured) @@ -86,7 +86,7 @@ BUGSINK = { # "MAX_ENVELOPE_SIZE": 100 * _MEBIBYTE, # "MAX_ENVELOPE_COMPRESSED_SIZE": 20 * _MEBIBYTE, - "BASE_URL": "http://bugsink:8000", # no trailing slash + "BASE_URL": "http://bugsink:8000/foobar", # no trailing slash "SITE_TITLE": "Bugsink", # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company" # undocumented feature: this enables links to the admin interface in the header/footer. I'm not sure where the admin @@ -151,3 +151,11 @@ ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"]) # django-tailwind setting; the below allows for environment-variable overriding of the npm binary path. NPM_BIN_PATH = os.getenv("NPM_BIN_PATH", "npm") + + +FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"]) +if FORCE_SCRIPT_NAME: + # "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME + # automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028 + # so we'll just be explicit about it. + STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/" diff --git a/bugsink/utils.py b/bugsink/utils.py index 6a64e05..2aeb3ab 100644 --- a/bugsink/utils.py +++ b/bugsink/utils.py @@ -77,6 +77,27 @@ def deduce_allowed_hosts(base_url): return [url.hostname] + ["localhost", "127.0.0.1"] +def deduce_script_name(base_url): + """Extract the path prefix from BASE_URL for subpath hosting support.""" + + # On the matter of leading an trailing slashes: + # https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.13 (the CGI spec) -> SCRIPT_NAME must start with a / + # trailing slash: doesn't matter https://github.com/django/django/commit/a15a3e9148e9 (but normalized away) + # So: leading-but-no-trailing slash is what we want. + # Our usage in STATIC_URL is made consistent with that. + # Because BASE_URL is documented to be "no trailing slash", the below produces exactly what we want. + + try: + parsed_url = urlparse(base_url) + path = parsed_url.path + except Exception: + # maximize robustness here: one broken setting shouldn't break the deduction for others (the brokenness of + # BASE_URL will be manifested elsewhere more explicitly anyway) + return None + + return path if path not in (None, "", "/") else None + + # Note: the excessive string-matching in the below is intentional: # I'd rather have our error-handling code as simple as possible # instead of relying on all kinds of imports of Exception classes. diff --git a/issues/templates/issues/base.html b/issues/templates/issues/base.html index e4d1704..9fbd9db 100644 --- a/issues/templates/issues/base.html +++ b/issues/templates/issues/base.html @@ -109,13 +109,13 @@ {# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#}
{# 96rem is 1536px, which matches the 2xl class; this is no "must" but eyeballing revealed: good result #}
@@ -127,16 +127,16 @@ {% if is_event_page %}
{% blocktranslate with digest_order=event.digest_order|intcomma total_events=issue.digested_event_count|intcomma ingested_at=event.ingested_at|date:"j M G:i T" %}Event {{ digest_order }} of {{ total_events }} which occured at {{ ingested_at }}{% endblocktranslate %}
{% endif %}
{% if is_event_page %} - {% translate "Download" %} - | {% translate "JSON" %} - | {% translate "Plain" %} + {% translate "Download" %} + | {% translate "JSON" %} + | {% translate "Plain" %} {% endif %} {% if app_settings.USE_ADMIN and user.is_staff %} {% if is_event_page %} - | {% translate "Event Admin" %} | + | {% translate "Event Admin" %} | {% endif %} - {% translate "Issue Admin" %} + {% translate "Issue Admin" %} {% endif %}
diff --git a/issues/templates/issues/event_list.html b/issues/templates/issues/event_list.html index 6ffb91c..2522eea 100644 --- a/issues/templates/issues/event_list.html +++ b/issues/templates/issues/event_list.html @@ -119,11 +119,11 @@ TODO - {{ event.digest_order }} + {{ event.digest_order }} {# how useful is this really? #} - {{ event.id|truncatechars:9 }} + {{ event.id|truncatechars:9 }} diff --git a/issues/templates/issues/issue_list.html b/issues/templates/issues/issue_list.html index 74ae385..b2261ea 100644 --- a/issues/templates/issues/issue_list.html +++ b/issues/templates/issues/issue_list.html @@ -170,7 +170,7 @@
- {% if issue.is_resolved %} + {% if issue.is_resolved %} {% endif %}{% if issue.is_muted %}   {% endif %}{{ issue.title|truncatechars:100 }} diff --git a/projects/templates/projects/project_list.html b/projects/templates/projects/project_list.html index bb0b89c..3936486 100644 --- a/projects/templates/projects/project_list.html +++ b/projects/templates/projects/project_list.html @@ -64,7 +64,7 @@
{% if project.member or request.user.is_superuser %} - {{ project.name }} + {{ project.name }} {% else %} {{ project.name }} {% endif %} diff --git a/templates/bugsink/login.html b/templates/bugsink/login.html index d515fc6..7f355fd 100644 --- a/templates/bugsink/login.html +++ b/templates/bugsink/login.html @@ -9,7 +9,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo
diff --git a/templates/signup.html b/templates/signup.html index ccf12a7..2215331 100644 --- a/templates/signup.html +++ b/templates/signup.html @@ -9,7 +9,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo
diff --git a/theme/templates/bare_base.html b/theme/templates/bare_base.html index 588ddbc..068d7b3 100644 --- a/theme/templates/bare_base.html +++ b/theme/templates/bare_base.html @@ -26,8 +26,8 @@
- Bugsink logo -
Bugsink
+ Bugsink logo +
Bugsink
{% block content %}{% endblock %} diff --git a/theme/templates/base.html b/theme/templates/base.html index 245bfce..f2b3e8c 100644 --- a/theme/templates/base.html +++ b/theme/templates/base.html @@ -27,8 +27,8 @@
- Bugsink logo -
{{ site_title }}
+ Bugsink logo +
{{ site_title }}
{% if not app_settings.SINGLE_TEAM %}
{% translate "Teams" %}
@@ -42,18 +42,18 @@
{% if app_settings.USE_ADMIN and user.is_staff %} -
{% translate "Admin" %}
+
{% translate "Admin" %}
{% endif %} {% if user.is_superuser %} -
{% translate "Users" %}
-
{% translate "Tokens" %}
+
{% translate "Users" %}
+
{% translate "Tokens" %}
{% endif %} {% if logged_in_user.is_anonymous %} -
{% translate "Login" %}
{# I don't think this is actually ever shown in practice, because you must always be logged in #} +
{% translate "Login" %}
{# I don't think this is actually ever shown in practice, because you must always be logged in #} {% else %} -
{% translate "Preferences" %}
+
{% translate "Preferences" %}
{% csrf_token %}
{% endif %}
diff --git a/users/templates/users/confirm_email.html b/users/templates/users/confirm_email.html index 7175a22..a6ba118 100644 --- a/users/templates/users/confirm_email.html +++ b/users/templates/users/confirm_email.html @@ -9,7 +9,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo
diff --git a/users/templates/users/confirm_email_sent.html b/users/templates/users/confirm_email_sent.html index 01adc91..3523a2b 100644 --- a/users/templates/users/confirm_email_sent.html +++ b/users/templates/users/confirm_email_sent.html @@ -9,7 +9,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo
diff --git a/users/templates/users/logged_out.html b/users/templates/users/logged_out.html index 12e52b1..c8ee39a 100644 --- a/users/templates/users/logged_out.html +++ b/users/templates/users/logged_out.html @@ -9,7 +9,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo
diff --git a/users/templates/users/request_reset_password.html b/users/templates/users/request_reset_password.html index 9704c53..f563131 100644 --- a/users/templates/users/request_reset_password.html +++ b/users/templates/users/request_reset_password.html @@ -10,7 +10,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo
diff --git a/users/templates/users/resend_confirmation.html b/users/templates/users/resend_confirmation.html index 85a3366..bc64c19 100644 --- a/users/templates/users/resend_confirmation.html +++ b/users/templates/users/resend_confirmation.html @@ -10,7 +10,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo
diff --git a/users/templates/users/reset_password.html b/users/templates/users/reset_password.html index 5d2eb34..5c5d639 100644 --- a/users/templates/users/reset_password.html +++ b/users/templates/users/reset_password.html @@ -10,7 +10,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo
diff --git a/users/templates/users/reset_password_email_sent.html b/users/templates/users/reset_password_email_sent.html index 9bc7a14..5132d7c 100644 --- a/users/templates/users/reset_password_email_sent.html +++ b/users/templates/users/reset_password_email_sent.html @@ -9,7 +9,7 @@
{# the cyan background #}
{# the centered box #}
{# the logo #} - Bugsink logo + Bugsink logo