## Summary - Supports [chat_web_app#22](ai_ml_operations/chat_web_app#22) - Always merge Capacitor WebView origins (`https://localhost`, `capacitor://localhost`, `http://localhost`) into `CORS_ALLOWED_ORIGINS` and `CSRF_TRUSTED_ORIGINS` - Keep `CORS_ALLOW_CREDENTIALS = False` (JWT header-only; no cookie credentials) ## Test plan - [x] `uv run python manage.py test chat_backend.tests.test_settings_cors` - [ ] OPTIONS preflight from `https://localhost` / `capacitor://localhost` against prod/beta with `CORS_ORIGIN_ALLOW_ALL=false` - [ ] Authenticated JWT GET/POST from Capacitor shell after FE #22 landsReviewed-on: #19
This commit was merged in pull request #19.
This commit is contained in:
@@ -18,6 +18,7 @@ DJANGO_DEBUG=false
|
|||||||
DJANGO_SECRET_KEY=replace-with-a-long-random-secret
|
DJANGO_SECRET_KEY=replace-with-a-long-random-secret
|
||||||
DJANGO_ALLOWED_HOSTS=chatbackend.aimloperations.com
|
DJANGO_ALLOWED_HOSTS=chatbackend.aimloperations.com
|
||||||
# Optional override; when unset, https:// origins are derived from DJANGO_ALLOWED_HOSTS.
|
# Optional override; when unset, https:// origins are derived from DJANGO_ALLOWED_HOSTS.
|
||||||
|
# Capacitor WebView origins (https://localhost, capacitor://localhost) are always merged in code.
|
||||||
# DJANGO_CSRF_TRUSTED_ORIGINS=https://chatbackend.aimloperations.com,https://chat.aimloperations.com
|
# DJANGO_CSRF_TRUSTED_ORIGINS=https://chatbackend.aimloperations.com,https://chat.aimloperations.com
|
||||||
CORS_ALLOWED_ORIGINS=https://chat.aimloperations.com
|
CORS_ALLOWED_ORIGINS=https://chat.aimloperations.com
|
||||||
CORS_ORIGIN_ALLOW_ALL=false
|
CORS_ORIGIN_ALLOW_ALL=false
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from llm_be.settings import (
|
||||||
|
CAPACITOR_WEBVIEW_ORIGINS,
|
||||||
|
build_csrf_trusted_origins,
|
||||||
|
with_capacitor_webview_origins,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildCsrfTrustedOriginsTests(SimpleTestCase):
|
||||||
|
def test_derives_https_for_public_hosts(self):
|
||||||
|
origins = build_csrf_trusted_origins(
|
||||||
|
["chat.aimloperations.com", "chatbackend.aimloperations.com"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
origins,
|
||||||
|
[
|
||||||
|
"https://chat.aimloperations.com",
|
||||||
|
"https://chatbackend.aimloperations.com",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_derives_http_for_localhost(self):
|
||||||
|
origins = build_csrf_trusted_origins(["localhost:8003", "127.0.0.1"])
|
||||||
|
self.assertEqual(
|
||||||
|
origins,
|
||||||
|
["http://localhost:8003", "http://127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_explicit_list_wins(self):
|
||||||
|
origins = build_csrf_trusted_origins(
|
||||||
|
["ignored.example"],
|
||||||
|
["https://chat.aimloperations.com"],
|
||||||
|
)
|
||||||
|
self.assertEqual(origins, ["https://chat.aimloperations.com"])
|
||||||
|
|
||||||
|
|
||||||
|
class CapacitorWebviewOriginTests(SimpleTestCase):
|
||||||
|
def test_appends_capacitor_origins(self):
|
||||||
|
merged = with_capacitor_webview_origins(["https://chat.aimloperations.com"])
|
||||||
|
self.assertEqual(merged[0], "https://chat.aimloperations.com")
|
||||||
|
for origin in CAPACITOR_WEBVIEW_ORIGINS:
|
||||||
|
self.assertIn(origin, merged)
|
||||||
|
|
||||||
|
def test_does_not_duplicate_existing(self):
|
||||||
|
seed = ["https://localhost", "https://chat.aimloperations.com"]
|
||||||
|
merged = with_capacitor_webview_origins(seed)
|
||||||
|
self.assertEqual(merged.count("https://localhost"), 1)
|
||||||
|
self.assertIn("capacitor://localhost", merged)
|
||||||
|
|
||||||
|
def test_settings_include_capacitor_origins(self):
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
for origin in CAPACITOR_WEBVIEW_ORIGINS:
|
||||||
|
self.assertIn(origin, settings.CORS_ALLOWED_ORIGINS)
|
||||||
|
self.assertIn(origin, settings.CSRF_TRUSTED_ORIGINS)
|
||||||
|
|
||||||
|
def test_cors_credentials_disabled_for_jwt_only(self):
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
self.assertFalse(settings.CORS_ALLOW_CREDENTIALS)
|
||||||
@@ -74,11 +74,20 @@ def database_config() -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Capacitor WebView origins (Android https://localhost, iOS capacitor://localhost).
|
||||||
|
# Required so JWT API calls from native shells pass CORS/CSRF origin checks (#22).
|
||||||
|
CAPACITOR_WEBVIEW_ORIGINS = (
|
||||||
|
"https://localhost",
|
||||||
|
"capacitor://localhost",
|
||||||
|
"http://localhost",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_csrf_trusted_origins(
|
def build_csrf_trusted_origins(
|
||||||
allowed_hosts: list[str], explicit: list[str] | None = None
|
allowed_hosts: list[str], explicit: list[str] | None = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
if explicit:
|
if explicit:
|
||||||
return explicit
|
return list(explicit)
|
||||||
|
|
||||||
local_hosts = {"localhost", "127.0.0.1", "0.0.0.0"}
|
local_hosts = {"localhost", "127.0.0.1", "0.0.0.0"}
|
||||||
origins: list[str] = []
|
origins: list[str] = []
|
||||||
@@ -91,6 +100,17 @@ def build_csrf_trusted_origins(
|
|||||||
return origins
|
return origins
|
||||||
|
|
||||||
|
|
||||||
|
def with_capacitor_webview_origins(origins: list[str]) -> list[str]:
|
||||||
|
"""Append Capacitor WebView origins without duplicates (order preserved)."""
|
||||||
|
seen = set(origins)
|
||||||
|
merged = list(origins)
|
||||||
|
for origin in CAPACITOR_WEBVIEW_ORIGINS:
|
||||||
|
if origin not in seen:
|
||||||
|
merged.append(origin)
|
||||||
|
seen.add(origin)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
DJANGO_ENV = (env("DJANGO_ENV", "dev") or "dev").lower()
|
DJANGO_ENV = (env("DJANGO_ENV", "dev") or "dev").lower()
|
||||||
|
|
||||||
SECRET_KEY = env(
|
SECRET_KEY = env(
|
||||||
@@ -106,16 +126,21 @@ allowed_hosts = env_list(
|
|||||||
)
|
)
|
||||||
ALLOWED_HOSTS = allowed_hosts if allowed_hosts else ["*"]
|
ALLOWED_HOSTS = allowed_hosts if allowed_hosts else ["*"]
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = build_csrf_trusted_origins(
|
CSRF_TRUSTED_ORIGINS = with_capacitor_webview_origins(
|
||||||
|
build_csrf_trusted_origins(
|
||||||
ALLOWED_HOSTS,
|
ALLOWED_HOSTS,
|
||||||
env_list("DJANGO_CSRF_TRUSTED_ORIGINS"),
|
env_list("DJANGO_CSRF_TRUSTED_ORIGINS"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# JWT Authorization header only — no cookie credentials from the SPA/Capacitor shell.
|
||||||
CORS_ALLOW_CREDENTIALS = False
|
CORS_ALLOW_CREDENTIALS = False
|
||||||
CORS_ORIGIN_ALLOW_ALL = env_bool("CORS_ORIGIN_ALLOW_ALL", True)
|
CORS_ORIGIN_ALLOW_ALL = env_bool("CORS_ORIGIN_ALLOW_ALL", True)
|
||||||
CORS_ALLOWED_ORIGINS = env_list(
|
CORS_ALLOWED_ORIGINS = with_capacitor_webview_origins(
|
||||||
|
env_list(
|
||||||
"CORS_ALLOWED_ORIGINS",
|
"CORS_ALLOWED_ORIGINS",
|
||||||
"http://localhost:3000,http://127.0.0.1:3000,https://chat.aimloperations.com",
|
"http://localhost:3000,http://127.0.0.1:3000,https://chat.aimloperations.com",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ollama — GPU host on LAN for deployed envs; loopback for local Ollama.
|
# Ollama — GPU host on LAN for deployed envs; loopback for local Ollama.
|
||||||
|
|||||||
Reference in New Issue
Block a user