diff --git a/src/config.py b/src/config.py index b959a4e8..efb53c42 100644 --- a/src/config.py +++ b/src/config.py @@ -149,6 +149,12 @@ class Settings(BaseSettings): "http://127.0.0.1:8000", ] + # ── Matrix Frontend Integration ──────────────────────────────────────── + # URL of the Matrix frontend (Replit/Tailscale) for CORS. + # When set, this origin is added to CORS allowed_origins. + # Example: "http://100.124.176.28:8080" or "https://alexanderwhitestone.com" + matrix_frontend_url: str = "" # Empty = disabled + # Trusted hosts for the Host header check (TrustedHostMiddleware). # Set TRUSTED_HOSTS as a comma-separated list. Wildcards supported (e.g. "*.ts.net"). # Defaults include localhost + Tailscale MagicDNS. Add your Tailscale IP if needed. diff --git a/src/dashboard/app.py b/src/dashboard/app.py index 868a5b61..3bf6ac3f 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -10,6 +10,7 @@ Key improvements: import asyncio import json import logging +import re from contextlib import asynccontextmanager from pathlib import Path @@ -520,17 +521,40 @@ app = FastAPI( def _get_cors_origins() -> list[str]: - """Get CORS origins from settings, rejecting wildcards in production.""" - origins = settings.cors_origins + """Get CORS origins from settings, rejecting wildcards in production. + + Adds matrix_frontend_url when configured. Always allows Tailscale IPs + (100.x.x.x range) for development convenience. + """ + origins = list(settings.cors_origins) + + # Strip wildcards in production (security) if "*" in origins and not settings.debug: logger.warning( "Wildcard '*' in CORS_ORIGINS stripped in production — " "set explicit origins via CORS_ORIGINS env var" ) origins = [o for o in origins if o != "*"] + + # Add Matrix frontend URL if configured + if settings.matrix_frontend_url: + url = settings.matrix_frontend_url.strip() + if url and url not in origins: + origins.append(url) + logger.debug("Added Matrix frontend to CORS: %s", url) + return origins +# Pattern to match Tailscale IPs (100.x.x.x) for CORS origin regex +_TAILSCALE_IP_PATTERN = re.compile(r"^https?://100\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d+)?$") + + +def _is_tailscale_origin(origin: str) -> bool: + """Check if origin is a Tailscale IP (100.x.x.x range).""" + return bool(_TAILSCALE_IP_PATTERN.match(origin)) + + # Add dedicated middleware in correct order # 1. Logging (outermost to capture everything) app.add_middleware(RequestLoggingMiddleware, skip_paths=["/health"]) @@ -552,6 +576,7 @@ app.add_middleware( app.add_middleware( CORSMiddleware, allow_origins=_get_cors_origins(), + allow_origin_regex=r"https?://100\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?", allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "Authorization"],