157 lines
6.3 KiB
Python
157 lines
6.3 KiB
Python
"""
|
|
Patch for MFE Config API to support multi-tenant dynamic configuration.
|
|
|
|
This module patches the MFE Config API to inject tenant-specific configuration
|
|
based on the X-MFE-Origin header (from webpack proxy) or Host header.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from functools import wraps
|
|
|
|
|
|
def get_host_from_request(request):
|
|
"""
|
|
Extract the tenant host from the request.
|
|
|
|
Priority:
|
|
1. X-MFE-Origin header (set by webpack proxy - preserves original MFE tenant host)
|
|
2. request.get_host() (standard Host header)
|
|
"""
|
|
mfe_origin = request.META.get('HTTP_X_MFE_ORIGIN', '')
|
|
if mfe_origin:
|
|
# Parse origin (e.g., "http://mondaytest.apps.local.openedx.io:1999") to get host
|
|
return mfe_origin.split('://', 1)[-1]
|
|
return request.get_host()
|
|
|
|
|
|
def get_tenant_config(host):
|
|
"""
|
|
Generate tenant-specific MFE configuration based on request host.
|
|
|
|
Args:
|
|
host: The request host (e.g., 'talent1.apps.local.openedx.io:1999')
|
|
|
|
Returns:
|
|
dict or None: Tenant config if recognized, None otherwise
|
|
"""
|
|
# Pattern for MFE subdomain: talent1.apps.local.openedx.io
|
|
mfe_match = re.match(r'^(\w+)\.apps\.local\.openedx\.io(:\d+)?$', host)
|
|
if mfe_match:
|
|
tenant = mfe_match.group(1)
|
|
if tenant not in ['local', 'studio', 'apps', 'meilisearch', 'www']:
|
|
return _generate_config(tenant)
|
|
|
|
# Pattern for LMS subdomain: talent1.local.openedx.io
|
|
lms_match = re.match(r'^(\w+)\.local\.openedx\.io(:\d+)?$', host)
|
|
if lms_match:
|
|
tenant = lms_match.group(1)
|
|
if tenant not in ['local', 'studio', 'apps', 'meilisearch', 'www']:
|
|
return _generate_config(tenant)
|
|
|
|
return None
|
|
|
|
|
|
def _generate_config(tenant):
|
|
"""Generate MFE configuration for a tenant."""
|
|
lms_domain = f"{tenant}.local.openedx.io"
|
|
lms_url = f"http://{lms_domain}:8000"
|
|
apps_base = f"http://{tenant}.apps.local.openedx.io"
|
|
|
|
# Look up the Site's display name from the database, fallback to "TenantName Learning"
|
|
site_name = lms_domain
|
|
try:
|
|
from django.contrib.sites.models import Site
|
|
site = Site.objects.filter(domain=lms_domain).first()
|
|
if site and site.name and site.name != site.domain:
|
|
site_name = site.name
|
|
else:
|
|
# No matching Site or Site has same name as domain — use "Mondaytest Learning" style
|
|
site_name = f"{tenant.replace('-', ' ').title()} Learning"
|
|
except Exception:
|
|
site_name = f"{tenant.replace('-', ' ').title()} Learning"
|
|
|
|
return {
|
|
"BASE_URL": apps_base,
|
|
"LMS_BASE_URL": lms_url,
|
|
"SITE_NAME": site_name,
|
|
"PLATFORM_NAME": site_name,
|
|
"LOGIN_URL": f"{lms_url}/login",
|
|
"LOGOUT_URL": f"{lms_url}/logout",
|
|
"REFRESH_ACCESS_TOKEN_ENDPOINT": f"{lms_url}/login_refresh",
|
|
"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",
|
|
"LEARNER_HOME_MICROFRONTEND_URL": f"{apps_base}:1996/learner-dashboard/",
|
|
"ACCOUNT_MICROFRONTEND_URL": f"{apps_base}:1997/account/",
|
|
"ACCOUNT_SETTINGS_URL": f"{apps_base}:1997/account/",
|
|
"PROFILE_MICROFRONTEND_URL": f"{apps_base}:1995/profile/u/",
|
|
"AUTHN_MICROFRONTEND_URL": f"{apps_base}:1999/authn",
|
|
"LEARNING_MICROFRONTEND_URL": f"{apps_base}:2000/learning",
|
|
"LEARNING_BASE_URL": f"{apps_base}:2000/learning",
|
|
"COURSE_AUTHORING_MICROFRONTEND_URL": f"{apps_base}:2001/authoring",
|
|
"DISCUSSIONS_MICROFRONTEND_URL": f"{apps_base}:2002/discussions",
|
|
"DISCUSSIONS_MFE_BASE_URL": f"{apps_base}:2002/discussions",
|
|
"WRITABLE_GRADEBOOK_URL": f"{apps_base}:1994/gradebook",
|
|
"COMMUNICATIONS_MICROFRONTEND_URL": f"{apps_base}:1984/communications",
|
|
"ORA_GRADING_MICROFRONTEND_URL": f"{apps_base}:1993/ora-grading",
|
|
"ADMIN_CONSOLE_MICROFRONTEND_URL": f"{apps_base}:2025/admin-console",
|
|
"ADMIN_CONSOLE_URL": f"{apps_base}:2025/admin-console",
|
|
"ACCOUNT_PROFILE_URL": f"{apps_base}:1995/profile",
|
|
}
|
|
|
|
|
|
def patch_mfe_config_api():
|
|
"""
|
|
Patch the MFE Config API to inject tenant-specific configuration.
|
|
|
|
This patches the MFEConfigView.get() method to inject tenant-specific
|
|
configuration based on the X-MFE-Origin header or Host header.
|
|
"""
|
|
try:
|
|
from lms.djangoapps.mfe_config_api.views import MFEConfigView
|
|
|
|
# Store the original get method
|
|
original_get = MFEConfigView.get
|
|
|
|
@wraps(original_get)
|
|
def patched_get(self, request, *args, **kwargs):
|
|
"""Patched get that injects tenant config."""
|
|
# Get tenant config using X-MFE-Origin header (from webpack proxy) or Host header
|
|
host = get_host_from_request(request)
|
|
tenant_config = get_tenant_config(host)
|
|
|
|
# Call original get
|
|
response = original_get(self, request, *args, **kwargs)
|
|
|
|
# Inject tenant config into the JsonResponse content
|
|
if tenant_config and hasattr(response, 'content'):
|
|
try:
|
|
# Parse the JSON response and inject tenant config
|
|
content = response.content.decode('utf-8')
|
|
data = json.loads(content)
|
|
# Merge tenant config (tenant overrides take precedence)
|
|
data.update(tenant_config)
|
|
# Re-create the response with updated content
|
|
from django.http import JsonResponse
|
|
new_response = JsonResponse(data, status=response.status_code)
|
|
# Copy headers
|
|
for header, value in response.items():
|
|
if header.lower() not in ('content-type', 'content-length'):
|
|
new_response[header] = value
|
|
return new_response
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
pass
|
|
|
|
return response
|
|
|
|
# Apply the patch
|
|
MFEConfigView.get = patched_get
|
|
print("[PatchMFEConfig] MFE Config API patched successfully for multi-tenant support")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"[PatchMFEConfig] Failed to patch MFE Config API: {e}")
|
|
return False
|