refactor: rename platform_domain → main_domain to avoid confusion with platform.domain
Some checks failed
Some checks failed
The setting `settings.platform_domain` (the global/main domain like "wizard.lu") was easily confused with `platform.domain` (per-platform domain like "rewardflow.lu"). Renamed to `settings.main_domain` / `MAIN_DOMAIN` env var across the entire codebase. Also updated docs to reflect the refactored store detection logic with `is_platform_domain` / `is_subdomain_of_platform` guards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,10 +67,10 @@ LOG_LEVEL=INFO
|
|||||||
LOG_FILE=logs/app.log
|
LOG_FILE=logs/app.log
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# PLATFORM DOMAIN CONFIGURATION
|
# MAIN DOMAIN CONFIGURATION
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Your main platform domain
|
# Your main platform domain
|
||||||
PLATFORM_DOMAIN=wizard.lu
|
MAIN_DOMAIN=wizard.lu
|
||||||
|
|
||||||
# Custom domain features
|
# Custom domain features
|
||||||
# Enable/disable custom domains
|
# Enable/disable custom domains
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This module provides classes and functions for:
|
|||||||
- Configuration management via environment variables
|
- Configuration management via environment variables
|
||||||
- Database settings
|
- Database settings
|
||||||
- JWT and authentication configuration
|
- JWT and authentication configuration
|
||||||
- Platform domain and multi-tenancy settings
|
- Main domain and multi-tenancy settings
|
||||||
- Admin initialization settings
|
- Admin initialization settings
|
||||||
|
|
||||||
Note: Environment detection is handled by app.core.environment module.
|
Note: Environment detection is handled by app.core.environment module.
|
||||||
@@ -94,9 +94,9 @@ class Settings(BaseSettings):
|
|||||||
log_file: str | None = None
|
log_file: str | None = None
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# PLATFORM DOMAIN CONFIGURATION
|
# MAIN DOMAIN CONFIGURATION
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
platform_domain: str = "wizard.lu"
|
main_domain: str = "wizard.lu"
|
||||||
|
|
||||||
# Custom domain features
|
# Custom domain features
|
||||||
allow_custom_domains: bool = True
|
allow_custom_domains: bool = True
|
||||||
@@ -353,7 +353,7 @@ def print_environment_info():
|
|||||||
print(f" Database: {settings.database_url}")
|
print(f" Database: {settings.database_url}")
|
||||||
print(f" Debug mode: {settings.debug}")
|
print(f" Debug mode: {settings.debug}")
|
||||||
print(f" API port: {settings.api_port}")
|
print(f" API port: {settings.api_port}")
|
||||||
print(f" Platform: {settings.platform_domain}")
|
print(f" Platform: {settings.main_domain}")
|
||||||
print(f" Secure cookies: {should_use_secure_cookies()}")
|
print(f" Secure cookies: {should_use_secure_cookies()}")
|
||||||
print("=" * 70 + "\n")
|
print("=" * 70 + "\n")
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ def purchase_addon(
|
|||||||
store = billing_service.get_store(db, store_id)
|
store = billing_service.get_store(db, store_id)
|
||||||
|
|
||||||
# Build URLs
|
# Build URLs
|
||||||
base_url = f"https://{settings.platform_domain}"
|
base_url = f"https://{settings.main_domain}"
|
||||||
success_url = f"{base_url}/store/{store.store_code}/billing?addon_success=true"
|
success_url = f"{base_url}/store/{store.store_code}/billing?addon_success=true"
|
||||||
cancel_url = f"{base_url}/store/{store.store_code}/billing?addon_cancelled=true"
|
cancel_url = f"{base_url}/store/{store.store_code}/billing?addon_cancelled=true"
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ def create_checkout_session(
|
|||||||
|
|
||||||
store_code = subscription_service.get_store_code(db, store_id)
|
store_code = subscription_service.get_store_code(db, store_id)
|
||||||
|
|
||||||
base_url = f"https://{settings.platform_domain}"
|
base_url = f"https://{settings.main_domain}"
|
||||||
success_url = f"{base_url}/store/{store_code}/billing?success=true"
|
success_url = f"{base_url}/store/{store_code}/billing?success=true"
|
||||||
cancel_url = f"{base_url}/store/{store_code}/billing?cancelled=true"
|
cancel_url = f"{base_url}/store/{store_code}/billing?cancelled=true"
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ def create_portal_session(
|
|||||||
merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id, current_user.token_platform_id)
|
merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id, current_user.token_platform_id)
|
||||||
|
|
||||||
store_code = subscription_service.get_store_code(db, store_id)
|
store_code = subscription_service.get_store_code(db, store_id)
|
||||||
return_url = f"https://{settings.platform_domain}/store/{store_code}/billing"
|
return_url = f"https://{settings.main_domain}/store/{store_code}/billing"
|
||||||
|
|
||||||
result = billing_service.create_portal_session(db, merchant_id, platform_id, return_url)
|
result = billing_service.create_portal_session(db, merchant_id, platform_id, return_url)
|
||||||
|
|
||||||
|
|||||||
@@ -617,7 +617,7 @@ class SignupService:
|
|||||||
|
|
||||||
# Build login URL
|
# Build login URL
|
||||||
login_url = (
|
login_url = (
|
||||||
f"https://{settings.platform_domain}"
|
f"https://{settings.main_domain}"
|
||||||
f"/store/{store.store_code}/dashboard"
|
f"/store/{store.store_code}/dashboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ def _build_base_context(
|
|||||||
"request": request,
|
"request": request,
|
||||||
"platform": platform,
|
"platform": platform,
|
||||||
"platform_name": settings.project_name,
|
"platform_name": settings.project_name,
|
||||||
"platform_domain": settings.platform_domain,
|
"main_domain": settings.main_domain,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add i18n globals
|
# Add i18n globals
|
||||||
|
|||||||
@@ -335,19 +335,19 @@ class Store(Base, TimestampMixin):
|
|||||||
Domain Resolution Priority:
|
Domain Resolution Priority:
|
||||||
1. Store-specific custom domain (StoreDomain) -> highest priority
|
1. Store-specific custom domain (StoreDomain) -> highest priority
|
||||||
2. Merchant domain (MerchantDomain) -> inherited default
|
2. Merchant domain (MerchantDomain) -> inherited default
|
||||||
3. Store subdomain ({store.subdomain}.{platform_domain}) -> fallback
|
3. Store subdomain ({store.subdomain}.{main_domain}) -> fallback
|
||||||
"""
|
"""
|
||||||
if self.primary_domain:
|
if self.primary_domain:
|
||||||
return self.primary_domain
|
return self.primary_domain
|
||||||
if self.merchant and self.merchant.primary_domain:
|
if self.merchant and self.merchant.primary_domain:
|
||||||
return self.merchant.primary_domain
|
return self.merchant.primary_domain
|
||||||
return f"{self.subdomain}.{settings.platform_domain}"
|
return f"{self.subdomain}.{settings.main_domain}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_domains(self):
|
def all_domains(self):
|
||||||
"""Get all active domains (subdomain + custom domains)."""
|
"""Get all active domains (subdomain + custom domains)."""
|
||||||
domains = [
|
domains = [
|
||||||
f"{self.subdomain}.{settings.platform_domain}"
|
f"{self.subdomain}.{settings.main_domain}"
|
||||||
] # Start with the main subdomain
|
] # Start with the main subdomain
|
||||||
for domain in self.domains:
|
for domain in self.domains:
|
||||||
if domain.is_active:
|
if domain.is_active:
|
||||||
|
|||||||
@@ -92,12 +92,15 @@ INFO Response: 200 for GET /admin/dashboard (0.143s)
|
|||||||
**Purpose**: Detect which store's storefront the request is for (multi-tenant core)
|
**Purpose**: Detect which store's storefront the request is for (multi-tenant core)
|
||||||
|
|
||||||
**What it does**:
|
**What it does**:
|
||||||
- Detects store from:
|
- Guards based on platform domain awareness:
|
||||||
- Custom domain (e.g., `customdomain.com`) — via `StoreDomain` lookup (optionally scoped to a platform via `StoreDomain.platform_id`)
|
- When `host == platform.domain` (e.g., `rewardflow.lu`): Methods 1 & 2 are skipped (the platform's own domain is not a store)
|
||||||
- Subdomain with two-step lookup:
|
- When host is a subdomain of `platform.domain` (e.g., `acme.rewardflow.lu`): Method 1 (custom domain) is skipped
|
||||||
1. `StorePlatform.custom_subdomain` — per-platform subdomain overrides (e.g., `wizatech-rewards.rewardflow.lu`)
|
- Detects store from (in priority order):
|
||||||
2. `Store.subdomain` — standard subdomain fallback (e.g., `wizatech.omsflow.lu`)
|
1. Custom domain (e.g., `customdomain.com`) — via `StoreDomain` lookup (skipped on platform domains)
|
||||||
- Path prefix (e.g., `/store/store1/` or `/stores/store1/`)
|
2. Subdomain with two-step lookup:
|
||||||
|
- `StorePlatform.custom_subdomain` — per-platform subdomain overrides (e.g., `wizatech-rewards.rewardflow.lu`)
|
||||||
|
- `Store.subdomain` — standard subdomain fallback (e.g., `wizatech.omsflow.lu`)
|
||||||
|
3. Path prefix (e.g., `/store/store1/` or `/stores/store1/`) — always runs as fallback
|
||||||
- Queries database to find store by domain or code
|
- Queries database to find store by domain or code
|
||||||
- Injects store object and platform into `request.state.store`
|
- Injects store object and platform into `request.state.store`
|
||||||
- Extracts "clean path" (path without store prefix)
|
- Extracts "clean path" (path without store prefix)
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ Every context includes these base variables regardless of modules:
|
|||||||
| `request` | FastAPI Request object |
|
| `request` | FastAPI Request object |
|
||||||
| `platform` | Platform model (may be None) |
|
| `platform` | Platform model (may be None) |
|
||||||
| `platform_name` | From settings.project_name |
|
| `platform_name` | From settings.project_name |
|
||||||
| `platform_domain` | From settings.platform_domain |
|
| `main_domain` | From settings.main_domain |
|
||||||
| `_` | Translation function (gettext style) |
|
| `_` | Translation function (gettext style) |
|
||||||
| `t` | Translation function (key-value style) |
|
| `t` | Translation function (key-value style) |
|
||||||
| `current_language` | Current language code |
|
| `current_language` | Current language code |
|
||||||
|
|||||||
@@ -113,26 +113,37 @@ platform.com/storefront/store3 → Store 3 Storefront
|
|||||||
|
|
||||||
### Store Detection Logic
|
### Store Detection Logic
|
||||||
|
|
||||||
The `StoreContextMiddleware` detects stores using this priority:
|
The `StoreContextMiddleware` detects stores via `_detect_store_from_host_and_path()`.
|
||||||
|
Before trying the three detection methods, two guards determine which methods apply:
|
||||||
|
|
||||||
|
- **`is_platform_domain`**: `host == platform.domain` (e.g., `rewardflow.lu` itself) — skips Methods 1 & 2
|
||||||
|
- **`is_subdomain_of_platform`**: host is a subdomain of `platform.domain` (e.g., `acme.rewardflow.lu`) — skips Method 1
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def detect_store(request):
|
def detect_store(host, path, platform):
|
||||||
host = request.headers.get("host")
|
platform_own_domain = platform.domain # e.g. "rewardflow.lu"
|
||||||
|
is_platform_domain = (host == platform_own_domain)
|
||||||
|
is_subdomain_of_platform = (
|
||||||
|
host != platform_own_domain
|
||||||
|
and host.endswith(f".{platform_own_domain}")
|
||||||
|
)
|
||||||
|
|
||||||
# 1. Try custom domain first
|
# 1. Custom domain — skipped when host is platform domain or subdomain of it
|
||||||
store = find_by_custom_domain(host)
|
if not is_platform_domain and not is_subdomain_of_platform:
|
||||||
if store:
|
main_domain = settings.main_domain # e.g. "wizard.lu"
|
||||||
return store, "custom_domain"
|
if host != main_domain and not host.endswith(f".{main_domain}"):
|
||||||
|
store = find_by_custom_domain(host)
|
||||||
|
if store:
|
||||||
|
return store, "custom_domain"
|
||||||
|
|
||||||
# 2. Try subdomain
|
# 2. Subdomain — skipped when host IS the platform domain
|
||||||
if host != settings.platform_domain:
|
if not is_platform_domain and "." in host:
|
||||||
store_code = host.split('.')[0]
|
subdomain = host.split('.')[0]
|
||||||
store = find_by_code(store_code)
|
store = find_by_code(subdomain)
|
||||||
if store:
|
if store:
|
||||||
return store, "subdomain"
|
return store, "subdomain"
|
||||||
|
|
||||||
# 3. Try path-based
|
# 3. Path-based — always runs as fallback
|
||||||
path = request.url.path
|
|
||||||
if path.startswith("/store/") or path.startswith("/storefront/"):
|
if path.startswith("/store/") or path.startswith("/storefront/"):
|
||||||
store_code = extract_code_from_path(path)
|
store_code = extract_code_from_path(path)
|
||||||
store = find_by_code(store_code)
|
store = find_by_code(store_code)
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ logger.info(f"Request: GET /storefront/products from 192.168.1.100")
|
|||||||
|
|
||||||
**What happens**:
|
**What happens**:
|
||||||
- Analyzes host header and path
|
- Analyzes host header and path
|
||||||
|
- Checks platform domain guards: if host matches `platform.domain`, Methods 1 & 2 are skipped; if host is a subdomain of `platform.domain`, Method 1 is skipped
|
||||||
- Determines routing mode (custom domain / subdomain / path-based)
|
- Determines routing mode (custom domain / subdomain / path-based)
|
||||||
- Queries database for store
|
- Queries database for store
|
||||||
- Extracts clean path
|
- Extracts clean path
|
||||||
|
|||||||
@@ -43,9 +43,10 @@ The core authentication manager handling JWT tokens, password hashing, and role-
|
|||||||
Detects and manages store context from custom domains, subdomains, or path-based routing. This is the foundation of the multi-tenant system.
|
Detects and manages store context from custom domains, subdomains, or path-based routing. This is the foundation of the multi-tenant system.
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
|
- Platform domain awareness: skips custom domain / subdomain detection when host is the platform's own domain
|
||||||
- Custom domain routing (customdomain.com → Store)
|
- Custom domain routing (customdomain.com → Store)
|
||||||
- Subdomain routing (store1.platform.com → Store)
|
- Subdomain routing (store1.platform.com → Store)
|
||||||
- Path-based routing (/store/store1/ → Store)
|
- Path-based routing (/store/store1/ → Store) — always runs as fallback
|
||||||
- Clean path extraction for nested routing
|
- Clean path extraction for nested routing
|
||||||
|
|
||||||
::: middleware.store_context.StoreContextManager
|
::: middleware.store_context.StoreContextManager
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ Controls the base domain for store subdomains and custom-domain features.
|
|||||||
|
|
||||||
| Variable | Description | Default | Required |
|
| Variable | Description | Default | Required |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `PLATFORM_DOMAIN` | Root domain under which store subdomains are created | `wizard.lu` | No |
|
| `MAIN_DOMAIN` | Root domain under which store subdomains are created | `wizard.lu` | No |
|
||||||
| `ALLOW_CUSTOM_DOMAINS` | Allow stores to use their own domain names | `True` | No |
|
| `ALLOW_CUSTOM_DOMAINS` | Allow stores to use their own domain names | `True` | No |
|
||||||
| `REQUIRE_DOMAIN_VERIFICATION` | Require DNS verification before activating a custom domain | `True` | No |
|
| `REQUIRE_DOMAIN_VERIFICATION` | Require DNS verification before activating a custom domain | `True` | No |
|
||||||
| `SSL_PROVIDER` | SSL certificate provider (`letsencrypt`, `cloudflare`, `manual`) | `letsencrypt` | No |
|
| `SSL_PROVIDER` | SSL certificate provider (`letsencrypt`, `cloudflare`, `manual`) | `letsencrypt` | No |
|
||||||
|
|||||||
@@ -911,7 +911,7 @@ settings.invitation_expiry_days # int
|
|||||||
settings.database_url # str
|
settings.database_url # str
|
||||||
|
|
||||||
# Platform
|
# Platform
|
||||||
settings.platform_domain # str
|
settings.main_domain # str
|
||||||
settings.project_name # str
|
settings.project_name # str
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -37,57 +37,80 @@ class StoreContextManager:
|
|||||||
"""
|
"""
|
||||||
Detect store context from request.
|
Detect store context from request.
|
||||||
|
|
||||||
Priority order:
|
Thin wrapper around _detect_store_from_host_and_path() that extracts
|
||||||
1. Custom domain (customdomain1.com)
|
host, path, and platform from the request object.
|
||||||
2. Subdomain (store1.platform.com)
|
|
||||||
3. Path-based (/store/store1/ or /stores/store1/)
|
|
||||||
|
|
||||||
Uses platform_clean_path from PlatformContextMiddleware when available.
|
|
||||||
This path has the platform prefix stripped (e.g., /oms/stores/foo → /stores/foo).
|
|
||||||
|
|
||||||
Returns dict with store info or None if not found.
|
Returns dict with store info or None if not found.
|
||||||
"""
|
"""
|
||||||
host = request.headers.get("host", "")
|
host = request.headers.get("host", "")
|
||||||
# Use platform_clean_path if available (set by PlatformContextMiddleware)
|
# Use platform_clean_path if available (set by PlatformContextMiddleware)
|
||||||
path = getattr(request.state, "platform_clean_path", None) or request.url.path
|
path = getattr(request.state, "platform_clean_path", None) or request.url.path
|
||||||
|
platform = getattr(request.state, "platform", None)
|
||||||
|
|
||||||
|
return StoreContextManager._detect_store_from_host_and_path(host, path, platform)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _detect_store_from_host_and_path(host: str, path: str, platform=None) -> dict | None:
|
||||||
|
"""
|
||||||
|
Core store detection logic from host and path.
|
||||||
|
|
||||||
|
Priority order:
|
||||||
|
1. Custom domain (customdomain1.com) — skipped on platform domains
|
||||||
|
2. Subdomain (store1.platform.com) — skipped on platform domains
|
||||||
|
3. Path-based (/store/store1/ or /stores/store1/) — always runs as fallback
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host: The request host header (may include port)
|
||||||
|
path: The clean path (platform prefix already stripped)
|
||||||
|
platform: Optional platform object from middleware state
|
||||||
|
|
||||||
|
Returns dict with store info or None if not found.
|
||||||
|
"""
|
||||||
|
original_host = host
|
||||||
|
|
||||||
# Remove port from host if present (e.g., localhost:8000 -> localhost)
|
# Remove port from host if present (e.g., localhost:8000 -> localhost)
|
||||||
if ":" in host:
|
if ":" in host:
|
||||||
host = host.split(":")[0]
|
host = host.split(":")[0]
|
||||||
|
|
||||||
# Method 1: Custom domain detection (HIGHEST PRIORITY)
|
# Determine if host is the platform's own domain or a subdomain of it
|
||||||
# Check if this is a custom domain (not platform.com and not localhost)
|
|
||||||
platform_domain = getattr(settings, "platform_domain", "platform.com")
|
|
||||||
|
|
||||||
# Skip custom domain detection if host is already a platform domain
|
|
||||||
# (e.g. rewardflow.lu is the loyalty platform, not a store)
|
# (e.g. rewardflow.lu is the loyalty platform, not a store)
|
||||||
platform = getattr(request.state, "platform", None)
|
# (e.g. acme.rewardflow.lu is a subdomain OF the platform, not a custom domain)
|
||||||
|
platform_own_domain = getattr(platform, "domain", None) if platform else None
|
||||||
is_platform_domain = (
|
is_platform_domain = (
|
||||||
platform and getattr(platform, "domain", None)
|
platform_own_domain and host == platform_own_domain
|
||||||
and host == platform.domain
|
)
|
||||||
|
is_subdomain_of_platform = (
|
||||||
|
platform_own_domain
|
||||||
|
and host != platform_own_domain
|
||||||
|
and host.endswith(f".{platform_own_domain}")
|
||||||
)
|
)
|
||||||
|
|
||||||
is_custom_domain = (
|
# Method 1: Custom domain detection (HIGHEST PRIORITY)
|
||||||
host
|
# Skip if host is a platform domain or a subdomain of one
|
||||||
and not is_platform_domain
|
if not is_platform_domain and not is_subdomain_of_platform:
|
||||||
and not host.endswith(f".{platform_domain}")
|
main_domain = getattr(settings, "main_domain", "platform.com")
|
||||||
and host != platform_domain
|
|
||||||
and host
|
|
||||||
not in ["localhost", "127.0.0.1", "admin.localhost", "admin.127.0.0.1"]
|
|
||||||
and not host.startswith("admin.")
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_custom_domain:
|
is_custom_domain = (
|
||||||
normalized_domain = StoreDomain.normalize_domain(host)
|
host
|
||||||
return {
|
and not host.endswith(f".{main_domain}")
|
||||||
"domain": normalized_domain,
|
and host != main_domain
|
||||||
"detection_method": "custom_domain",
|
and host
|
||||||
"host": host,
|
not in ["localhost", "127.0.0.1", "admin.localhost", "admin.127.0.0.1"]
|
||||||
"original_host": request.headers.get("host", ""),
|
and not host.startswith("admin.")
|
||||||
}
|
)
|
||||||
|
|
||||||
# Method 2: Subdomain detection (store1.platform.com)
|
if is_custom_domain:
|
||||||
if "." in host:
|
normalized_domain = StoreDomain.normalize_domain(host)
|
||||||
|
return {
|
||||||
|
"domain": normalized_domain,
|
||||||
|
"detection_method": "custom_domain",
|
||||||
|
"host": host,
|
||||||
|
"original_host": original_host,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method 2: Subdomain detection (acme.rewardflow.lu → "acme")
|
||||||
|
# Runs for subdomains of the platform domain, skipped for exact platform domain
|
||||||
|
if not is_platform_domain and "." in host:
|
||||||
parts = host.split(".")
|
parts = host.split(".")
|
||||||
# Check if it's a valid subdomain (not www, admin, api)
|
# Check if it's a valid subdomain (not www, admin, api)
|
||||||
if len(parts) >= 2 and parts[0] not in ["www", "admin", "api"]:
|
if len(parts) >= 2 and parts[0] not in ["www", "admin", "api"]:
|
||||||
@@ -349,12 +372,12 @@ class StoreContextManager:
|
|||||||
|
|
||||||
# Method 2: Subdomain detection from referer host
|
# Method 2: Subdomain detection from referer host
|
||||||
# orion.platform.com → orion
|
# orion.platform.com → orion
|
||||||
platform_domain = getattr(settings, "platform_domain", "platform.com")
|
main_domain = getattr(settings, "main_domain", "platform.com")
|
||||||
if "." in referer_host:
|
if "." in referer_host:
|
||||||
parts = referer_host.split(".")
|
parts = referer_host.split(".")
|
||||||
if len(parts) >= 2 and parts[0] not in ["www", "admin", "api"]:
|
if len(parts) >= 2 and parts[0] not in ["www", "admin", "api"]:
|
||||||
# Check if it's a subdomain of platform domain
|
# Check if it's a subdomain of main domain
|
||||||
if referer_host.endswith(f".{platform_domain}"):
|
if referer_host.endswith(f".{main_domain}"):
|
||||||
subdomain = parts[0]
|
subdomain = parts[0]
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[STORE] Extracted store from Referer subdomain: {subdomain}",
|
f"[STORE] Extracted store from Referer subdomain: {subdomain}",
|
||||||
@@ -374,8 +397,8 @@ class StoreContextManager:
|
|||||||
# custom-shop.com → custom-shop.com
|
# custom-shop.com → custom-shop.com
|
||||||
is_custom_domain = (
|
is_custom_domain = (
|
||||||
referer_host
|
referer_host
|
||||||
and not referer_host.endswith(f".{platform_domain}")
|
and not referer_host.endswith(f".{main_domain}")
|
||||||
and referer_host != platform_domain
|
and referer_host != main_domain
|
||||||
and referer_host not in ["localhost", "127.0.0.1"]
|
and referer_host not in ["localhost", "127.0.0.1"]
|
||||||
and not referer_host.startswith("admin.")
|
and not referer_host.startswith("admin.")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -390,14 +390,14 @@ def create_admin_settings(db: Session) -> int:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "platform_url",
|
"key": "platform_url",
|
||||||
"value": f"https://{settings.platform_domain}",
|
"value": f"https://{settings.main_domain}",
|
||||||
"value_type": "string",
|
"value_type": "string",
|
||||||
"description": "Main platform URL",
|
"description": "Main platform URL",
|
||||||
"is_public": True,
|
"is_public": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "support_email",
|
"key": "support_email",
|
||||||
"value": f"support@{settings.platform_domain}",
|
"value": f"support@{settings.main_domain}",
|
||||||
"value_type": "string",
|
"value_type": "string",
|
||||||
"description": "Platform support email",
|
"description": "Platform support email",
|
||||||
"is_public": True,
|
"is_public": True,
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ def validate_configuration(env_vars: dict) -> dict:
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Platform Domain
|
# Platform Domain
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
domain = env_vars.get("PLATFORM_DOMAIN", "orion.lu")
|
domain = env_vars.get("MAIN_DOMAIN", "orion.lu")
|
||||||
if domain != "orion.lu":
|
if domain != "orion.lu":
|
||||||
results["domain"] = {
|
results["domain"] = {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
@@ -350,7 +350,7 @@ def validate_configuration(env_vars: dict) -> dict:
|
|||||||
results["domain"] = {
|
results["domain"] = {
|
||||||
"status": "warning",
|
"status": "warning",
|
||||||
"message": "Using default domain",
|
"message": "Using default domain",
|
||||||
"items": ["Set PLATFORM_DOMAIN for your deployment"]
|
"items": ["Set MAIN_DOMAIN for your deployment"]
|
||||||
}
|
}
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1445,7 +1445,7 @@ def print_summary(db: Session):
|
|||||||
print("\n🏪 Demo Stores:")
|
print("\n🏪 Demo Stores:")
|
||||||
for store in stores:
|
for store in stores:
|
||||||
print(f"\n {store.name} ({store.store_code})")
|
print(f"\n {store.name} ({store.store_code})")
|
||||||
print(f" Subdomain: {store.subdomain}.{settings.platform_domain}")
|
print(f" Subdomain: {store.subdomain}.{settings.main_domain}")
|
||||||
|
|
||||||
# Show per-platform custom subdomains
|
# Show per-platform custom subdomains
|
||||||
from app.modules.tenancy.models import Platform
|
from app.modules.tenancy.models import Platform
|
||||||
|
|||||||
@@ -235,19 +235,19 @@ def print_dev_urls(platforms, stores, store_domains, store_platform_map):
|
|||||||
|
|
||||||
def print_prod_urls(platforms, stores, store_domains):
|
def print_prod_urls(platforms, stores, store_domains):
|
||||||
"""Print all production URLs."""
|
"""Print all production URLs."""
|
||||||
platform_domain = settings.platform_domain
|
main_domain = settings.main_domain
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("PRODUCTION URLS")
|
print("PRODUCTION URLS")
|
||||||
print(f"Platform domain: {platform_domain}")
|
print(f"Platform domain: {main_domain}")
|
||||||
print(SEPARATOR)
|
print(SEPARATOR)
|
||||||
|
|
||||||
# Admin
|
# Admin
|
||||||
print()
|
print()
|
||||||
print(" ADMIN PANEL")
|
print(" ADMIN PANEL")
|
||||||
print(f" Login: https://admin.{platform_domain}/admin/login")
|
print(f" Login: https://admin.{main_domain}/admin/login")
|
||||||
print(f" Dashboard: https://admin.{platform_domain}/admin/")
|
print(f" Dashboard: https://admin.{main_domain}/admin/")
|
||||||
print(f" API: https://admin.{platform_domain}/api/v1/admin/")
|
print(f" API: https://admin.{main_domain}/api/v1/admin/")
|
||||||
|
|
||||||
# Platforms
|
# Platforms
|
||||||
print()
|
print()
|
||||||
@@ -259,10 +259,10 @@ def print_prod_urls(platforms, stores, store_domains):
|
|||||||
print(f" Home: https://{p.domain}/")
|
print(f" Home: https://{p.domain}/")
|
||||||
elif p.code == "main":
|
elif p.code == "main":
|
||||||
print(f" {p.name}{tag}")
|
print(f" {p.name}{tag}")
|
||||||
print(f" Home: https://{platform_domain}/")
|
print(f" Home: https://{main_domain}/")
|
||||||
else:
|
else:
|
||||||
print(f" {p.name} ({p.code}){tag}")
|
print(f" {p.name} ({p.code}){tag}")
|
||||||
print(f" Home: https://{p.code}.{platform_domain}/")
|
print(f" Home: https://{p.code}.{main_domain}/")
|
||||||
|
|
||||||
# Group domains by store
|
# Group domains by store
|
||||||
domains_by_store = {}
|
domains_by_store = {}
|
||||||
@@ -280,7 +280,7 @@ def print_prod_urls(platforms, stores, store_domains):
|
|||||||
|
|
||||||
tag = f" [{status_badge(v.is_active)}]" if not v.is_active else ""
|
tag = f" [{status_badge(v.is_active)}]" if not v.is_active else ""
|
||||||
print(f" {v.name} ({v.store_code}){tag}")
|
print(f" {v.name} ({v.store_code}){tag}")
|
||||||
print(f" Dashboard: https://{v.subdomain}.{platform_domain}/store/{v.store_code}/")
|
print(f" Dashboard: https://{v.subdomain}.{main_domain}/store/{v.store_code}/")
|
||||||
|
|
||||||
# Storefronts
|
# Storefronts
|
||||||
print()
|
print()
|
||||||
@@ -295,7 +295,7 @@ def print_prod_urls(platforms, stores, store_domains):
|
|||||||
print(f" {v.name} ({v.store_code}){tag}")
|
print(f" {v.name} ({v.store_code}){tag}")
|
||||||
|
|
||||||
# Subdomain URL
|
# Subdomain URL
|
||||||
print(f" Subdomain: https://{v.subdomain}.{platform_domain}/")
|
print(f" Subdomain: https://{v.subdomain}.{main_domain}/")
|
||||||
|
|
||||||
# Custom domains
|
# Custom domains
|
||||||
vd_list = domains_by_store.get(v.id, [])
|
vd_list = domains_by_store.get(v.id, [])
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ if [ "$MODE" = "dev" ]; then
|
|||||||
fail ".env file not found — copy from .env.example"
|
fail ".env file not found — copy from .env.example"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REQUIRED_KEYS="DATABASE_URL REDIS_URL JWT_SECRET_KEY ADMIN_EMAIL PLATFORM_DOMAIN"
|
REQUIRED_KEYS="DATABASE_URL REDIS_URL JWT_SECRET_KEY ADMIN_EMAIL MAIN_DOMAIN"
|
||||||
for key in $REQUIRED_KEYS; do
|
for key in $REQUIRED_KEYS; do
|
||||||
val=$(env_val "$key")
|
val=$(env_val "$key")
|
||||||
if [ -n "$val" ]; then
|
if [ -n "$val" ]; then
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def client(db):
|
|||||||
with patch("middleware.store_context.get_db", override_get_db):
|
with patch("middleware.store_context.get_db", override_get_db):
|
||||||
with patch("middleware.theme_context.get_db", override_get_db):
|
with patch("middleware.theme_context.get_db", override_get_db):
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
yield client
|
yield client
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ def client(db):
|
|||||||
|
|
||||||
This patches:
|
This patches:
|
||||||
1. get_db in both middleware modules to use the test database
|
1. get_db in both middleware modules to use the test database
|
||||||
2. settings.platform_domain in store_context to use 'platform.com' for testing
|
2. settings.main_domain in store_context to use 'platform.com' for testing
|
||||||
|
|
||||||
This ensures middleware can see test fixtures and detect subdomains correctly.
|
This ensures middleware can see test fixtures and detect subdomains correctly.
|
||||||
"""
|
"""
|
||||||
@@ -64,7 +64,7 @@ def client(db):
|
|||||||
|
|
||||||
# Patch get_db in middleware modules - they have their own imports
|
# Patch get_db in middleware modules - they have their own imports
|
||||||
# The middleware calls: db_gen = get_db(); db = next(db_gen)
|
# The middleware calls: db_gen = get_db(); db = next(db_gen)
|
||||||
# Also patch settings.platform_domain so subdomain detection works with test hosts
|
# Also patch settings.main_domain so subdomain detection works with test hosts
|
||||||
# Also bypass StorefrontAccessMiddleware subscription check for test routes —
|
# Also bypass StorefrontAccessMiddleware subscription check for test routes —
|
||||||
# these tests verify store context detection, not subscription access.
|
# these tests verify store context detection, not subscription access.
|
||||||
from middleware.storefront_access import SKIP_PATH_PREFIXES
|
from middleware.storefront_access import SKIP_PATH_PREFIXES
|
||||||
@@ -76,7 +76,7 @@ def client(db):
|
|||||||
with patch("middleware.theme_context.get_db", override_get_db):
|
with patch("middleware.theme_context.get_db", override_get_db):
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
with patch("middleware.storefront_access.SKIP_PATH_PREFIXES", test_skip_prefixes):
|
with patch("middleware.storefront_access.SKIP_PATH_PREFIXES", test_skip_prefixes):
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ async def test_inactive_store_detection(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/platform-domain")
|
@router.get("/platform-domain")
|
||||||
async def test_platform_domain(request: Request):
|
async def test_main_domain(request: Request):
|
||||||
"""Test platform domain without subdomain."""
|
"""Test platform domain without subdomain."""
|
||||||
store = getattr(request.state, "store", None)
|
store = getattr(request.state, "store", None)
|
||||||
return {"store_detected": store is not None}
|
return {"store_detected": store is not None}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ for all routing modes: subdomain, custom domain, and path-based.
|
|||||||
|
|
||||||
Note: These tests require the middleware conftest.py which patches:
|
Note: These tests require the middleware conftest.py which patches:
|
||||||
1. get_db in middleware modules to use the test database session
|
1. get_db in middleware modules to use the test database session
|
||||||
2. settings.platform_domain to use 'platform.com' for testing subdomain detection
|
2. settings.main_domain to use 'platform.com' for testing subdomain detection
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -137,7 +137,7 @@ class TestStoreContextFlow:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["store_detected"] is False
|
assert data["store_detected"] is False
|
||||||
|
|
||||||
def test_platform_domain_without_subdomain_no_store(self, client):
|
def test_main_domain_without_subdomain_no_store(self, client):
|
||||||
"""Test that platform domain without subdomain doesn't detect store."""
|
"""Test that platform domain without subdomain doesn't detect store."""
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/middleware-test/platform-domain", headers={"host": "platform.com"}
|
"/middleware-test/platform-domain", headers={"host": "platform.com"}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class TestStoreContextManager:
|
|||||||
request.url = Mock(path="/")
|
request.url = Mock(path="/")
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ class TestStoreContextManager:
|
|||||||
request.url = Mock(path="/")
|
request.url = Mock(path="/")
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class TestStoreContextManager:
|
|||||||
request.url = Mock(path="/")
|
request.url = Mock(path="/")
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ class TestStoreContextManager:
|
|||||||
request.url = Mock(path="/")
|
request.url = Mock(path="/")
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class TestStoreContextManager:
|
|||||||
request.state.platform_clean_path = None
|
request.state.platform_clean_path = None
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ class TestStoreContextManager:
|
|||||||
request.state.platform_clean_path = None
|
request.state.platform_clean_path = None
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ class TestStoreContextManager:
|
|||||||
request.state.platform_clean_path = None
|
request.state.platform_clean_path = None
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ class TestStoreContextManager:
|
|||||||
request.state.platform_clean_path = None
|
request.state.platform_clean_path = None
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
@@ -441,7 +441,7 @@ class TestStoreContextManager:
|
|||||||
request.headers = {"referer": "http://orion.platform.com/storefront/products"} # noqa: SEC034
|
request.headers = {"referer": "http://orion.platform.com/storefront/products"} # noqa: SEC034
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.extract_store_from_referer(request)
|
context = StoreContextManager.extract_store_from_referer(request)
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ class TestStoreContextManager:
|
|||||||
request.headers = {"referer": "http://my-custom-shop.com/storefront/products"} # noqa: SEC034
|
request.headers = {"referer": "http://my-custom-shop.com/storefront/products"} # noqa: SEC034
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.extract_store_from_referer(request)
|
context = StoreContextManager.extract_store_from_referer(request)
|
||||||
|
|
||||||
@@ -490,7 +490,7 @@ class TestStoreContextManager:
|
|||||||
request.headers = {"referer": "http://admin.platform.com/dashboard"} # noqa: SEC034
|
request.headers = {"referer": "http://admin.platform.com/dashboard"} # noqa: SEC034
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.extract_store_from_referer(request)
|
context = StoreContextManager.extract_store_from_referer(request)
|
||||||
|
|
||||||
@@ -503,7 +503,7 @@ class TestStoreContextManager:
|
|||||||
request.headers = {"referer": "http://www.platform.com/storefront"} # noqa: SEC034
|
request.headers = {"referer": "http://www.platform.com/storefront"} # noqa: SEC034
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.extract_store_from_referer(request)
|
context = StoreContextManager.extract_store_from_referer(request)
|
||||||
|
|
||||||
@@ -515,7 +515,7 @@ class TestStoreContextManager:
|
|||||||
request.headers = {"referer": "http://localhost:8000/storefront"}
|
request.headers = {"referer": "http://localhost:8000/storefront"}
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.extract_store_from_referer(request)
|
context = StoreContextManager.extract_store_from_referer(request)
|
||||||
|
|
||||||
@@ -996,7 +996,7 @@ class TestEdgeCases:
|
|||||||
request.url = Mock(path="/")
|
request.url = Mock(path="/")
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "platform.com"
|
mock_settings.main_domain = "platform.com"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(request)
|
context = StoreContextManager.detect_store_context(request)
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class TestPlatformInjectionIntoStoreContext:
|
|||||||
mock_request.url = Mock(path="/store/login")
|
mock_request.url = Mock(path="/store/login")
|
||||||
|
|
||||||
with patch("middleware.store_context.settings") as mock_settings:
|
with patch("middleware.store_context.settings") as mock_settings:
|
||||||
mock_settings.platform_domain = "omsflow.lu"
|
mock_settings.main_domain = "omsflow.lu"
|
||||||
|
|
||||||
context = StoreContextManager.detect_store_context(mock_request)
|
context = StoreContextManager.detect_store_context(mock_request)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user