150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
"""
|
|
Dynamic MFE Configuration Middleware for Multi-Tenant Open edX
|
|
|
|
This middleware automatically injects tenant-specific MFE configuration
|
|
into the MFE Config API responses based on the request host.
|
|
|
|
Usage:
|
|
Add to MIDDLEWARE in settings:
|
|
MIDDLEWARE += ['mfe_dynamic_middleware.DynamicMFEConfigMiddleware']
|
|
|
|
This allows new tenants to work automatically without manual MFE_CONFIG_OVERRIDES configuration.
|
|
"""
|
|
|
|
import re
|
|
from django.conf import settings
|
|
|
|
|
|
class DynamicMFEConfigMiddleware:
|
|
"""
|
|
Middleware that dynamically injects tenant-specific MFE configuration.
|
|
|
|
This works by intercepting MFE Config API requests and injecting the
|
|
appropriate configuration based on the request's hostname.
|
|
"""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
# Compile regex patterns for performance
|
|
self.tenant_mfe_pattern = re.compile(r'^(\w+)\.apps\.local\.openedx\.io(:(\d+))?$')
|
|
self.tenant_lms_pattern = re.compile(r'^(\w+)\.local\.openedx\.io(:(\d+))?$')
|
|
|
|
def __call__(self, request):
|
|
"""Process the request and inject dynamic MFE config if needed."""
|
|
# Check if this is an MFE Config API request
|
|
if request.path.startswith('/api/mfe_config/v1'):
|
|
# Get tenant config and store it on the request
|
|
request.dynamic_mfe_config = self._get_tenant_config(request)
|
|
|
|
response = self.get_response(request)
|
|
return response
|
|
|
|
def _get_tenant_config(self, request):
|
|
"""
|
|
Get MFE configuration for the tenant based on request host.
|
|
|
|
Priority:
|
|
1. X-MFE-Origin header (set by webpack proxy - preserves original MFE tenant host)
|
|
2. request.get_host() (standard Host header)
|
|
"""
|
|
# Priority 1: X-MFE-Origin header from webpack proxy
|
|
mfe_origin = request.META.get('HTTP_X_MFE_ORIGIN', '')
|
|
if mfe_origin:
|
|
# Parse origin to get host
|
|
host = mfe_origin.split('://', 1)[-1]
|
|
else:
|
|
# Priority 2: Standard Host header
|
|
host = request.get_host()
|
|
|
|
# Try to extract tenant name from host
|
|
tenant = self._extract_tenant(host)
|
|
if not tenant:
|
|
return None
|
|
|
|
# Skip default/system tenants
|
|
if tenant in ['local', 'studio', 'apps', 'meilisearch', 'www']:
|
|
return None
|
|
|
|
# Generate MFE config for this tenant
|
|
return self._generate_config(tenant)
|
|
|
|
def _extract_tenant(self, host):
|
|
"""Extract tenant name from request host."""
|
|
# Pattern: <tenant>.apps.local.openedx.io:PORT (MFE)
|
|
match = self.tenant_mfe_pattern.match(host)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
# Pattern: <tenant>.local.openedx.io:PORT (LMS)
|
|
match = self.tenant_lms_pattern.match(host)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
return None
|
|
|
|
def _generate_config(self, tenant):
|
|
"""Generate MFE configuration for a tenant."""
|
|
base_domain = "local.openedx.io"
|
|
apps_domain = f"{tenant}.apps.local.openedx.io"
|
|
|
|
lms_url = f"http://{tenant}.{base_domain}:8000"
|
|
lms_domain = f"{tenant}.{base_domain}"
|
|
|
|
return {
|
|
"BASE_URL": lms_domain,
|
|
"LMS_BASE_URL": lms_url,
|
|
"SITE_NAME": f"{tenant}.{base_domain}",
|
|
|
|
# Auth
|
|
"LOGIN_URL": f"{lms_url}/login",
|
|
"LOGOUT_URL": f"{lms_url}/logout",
|
|
"REFRESH_ACCESS_TOKEN_ENDPOINT": f"{lms_url}/login_refresh",
|
|
|
|
# MFE URLs - use apps subdomain since MFEs are only accessible there
|
|
"AUTHN_MICROFRONTEND_URL": f"http://{apps_domain}:1999/authn",
|
|
"ACCOUNT_MICROFRONTEND_URL": f"http://{apps_domain}:1997/account/",
|
|
"PROFILE_MICROFRONTEND_URL": f"http://{apps_domain}:1995/profile/u/",
|
|
"LEARNING_MICROFRONTEND_URL": f"http://{apps_domain}:2000/learning",
|
|
"LEARNER_HOME_MICROFRONTEND_URL": f"http://{apps_domain}:1996/learner-dashboard/",
|
|
"COURSE_AUTHORING_MICROFRONTEND_URL": f"http://{apps_domain}:2001/authoring",
|
|
"DISCUSSIONS_MICROFRONTEND_URL": f"http://{apps_domain}:2002/discussions",
|
|
"WRITABLE_GRADEBOOK_URL": f"http://{apps_domain}:1994/gradebook",
|
|
"COMMUNICATIONS_MICROFRONTEND_URL": f"http://{apps_domain}:1984/communications",
|
|
"ORA_GRADING_MICROFRONTEND_URL": f"http://{apps_domain}:1993/ora-grading",
|
|
"ADMIN_CONSOLE_MICROFRONTEND_URL": f"http://{apps_domain}:2025/admin-console",
|
|
|
|
# Branding
|
|
"LOGO_URL": f"{lms_url}/theming/asset/images/logo.png",
|
|
"LOGO_TRADEMARK_URL": f"{lms_url}/theming/asset/images/logo.png",
|
|
"LOGO_WHITE_URL": f"{lms_url}/theming/asset/images/logo.png",
|
|
"FAVICON_URL": f"{lms_url}/favicon.ico",
|
|
|
|
# Other
|
|
"CSRF_TOKEN_API_PATH": "/csrf/api/v1/token",
|
|
"LANGUAGE_PREFERENCE_COOKIE_NAME": "openedx-language-preference",
|
|
"SESSION_COOKIE_DOMAIN": ".local.openedx.io",
|
|
}
|
|
|
|
|
|
def get_dynamic_mfe_config_for_api(request, mfe_name=None):
|
|
"""
|
|
Get dynamic MFE config for the MFE Config API.
|
|
|
|
This function can be called from the MFE Config API view to get
|
|
tenant-specific configuration.
|
|
|
|
Args:
|
|
request: Django HTTP request
|
|
mfe_name: Optional MFE name for MFE-specific overrides
|
|
|
|
Returns:
|
|
dict or None: Tenant-specific config, or None if not a tenant request
|
|
"""
|
|
# Check if middleware already processed this request
|
|
if hasattr(request, 'dynamic_mfe_config'):
|
|
return request.dynamic_mfe_config
|
|
|
|
# Otherwise, process it
|
|
middleware = DynamicMFEConfigMiddleware(lambda r: r)
|
|
return middleware._get_tenant_config(request)
|