- Fix IPv6 host parsing with _strip_port() utility - Remove dangerous StorePlatform→Store.subdomain silent fallback - Close storefront gate bypass when frontend_type is None - Add custom subdomain management UI and API for stores - Add domain health diagnostic tool - Convert db.add() in loops to db.add_all() (24 PERF-006 fixes) - Add tests for all new functionality (18 subdomain service tests) - Add .github templates for validator compliance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
233 lines
8.1 KiB
Python
233 lines
8.1 KiB
Python
# app/modules/dev_tools/services/domain_health_service.py
|
|
"""
|
|
Domain Health Check Service
|
|
|
|
Simulates the middleware resolution pipeline for every active access method
|
|
(custom subdomain, default subdomain, custom domain, path-based) to verify
|
|
they resolve to the expected store.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def run_domain_health_check(db: Session) -> dict:
|
|
"""
|
|
Check all active store access methods by simulating middleware resolution:
|
|
1. StorePlatform custom subdomains (acme-rewards.rewardflow.lu)
|
|
2. Default subdomains per platform (acme.omsflow.lu)
|
|
3. StoreDomain custom domains (wizatech.shop)
|
|
4. Path-based store routes (/storefront/{subdomain}/, /store/{subdomain}/)
|
|
|
|
Returns:
|
|
dict with keys: total, passed, failed, details (list of entry dicts)
|
|
"""
|
|
from app.modules.tenancy.models import Platform, Store, StoreDomain
|
|
from app.modules.tenancy.models.store_platform import StorePlatform
|
|
from middleware.store_context import StoreContextManager
|
|
|
|
details: list[dict] = []
|
|
|
|
# Pre-fetch all active stores, platforms, and memberships
|
|
active_stores = (
|
|
db.query(Store).filter(Store.is_active.is_(True)).all()
|
|
)
|
|
store_by_id: dict[int, Store] = {s.id: s for s in active_stores}
|
|
|
|
all_platforms = (
|
|
db.query(Platform).filter(Platform.is_active.is_(True)).all()
|
|
)
|
|
platform_by_id: dict[int, Platform] = {p.id: p for p in all_platforms}
|
|
|
|
all_store_platforms = (
|
|
db.query(StorePlatform)
|
|
.filter(StorePlatform.is_active.is_(True))
|
|
.all()
|
|
)
|
|
|
|
# Build store_id → list of (StorePlatform, Platform) for reuse
|
|
store_memberships: dict[int, list[tuple]] = {}
|
|
for sp in all_store_platforms:
|
|
platform = platform_by_id.get(sp.platform_id)
|
|
if platform:
|
|
store_memberships.setdefault(sp.store_id, []).append((sp, platform))
|
|
|
|
# ── 1. Custom subdomains (StorePlatform.custom_subdomain) ──
|
|
# e.g. acme-rewards.rewardflow.lu
|
|
for sp in all_store_platforms:
|
|
if not sp.custom_subdomain:
|
|
continue
|
|
expected_store = store_by_id.get(sp.store_id)
|
|
platform = platform_by_id.get(sp.platform_id)
|
|
if not expected_store or not platform:
|
|
continue
|
|
|
|
context = {
|
|
"detection_method": "subdomain",
|
|
"subdomain": sp.custom_subdomain,
|
|
"_platform": platform,
|
|
}
|
|
resolved = StoreContextManager.get_store_from_context(db, context)
|
|
|
|
passed = resolved is not None and resolved.id == expected_store.id
|
|
details.append(_entry(
|
|
domain=f"{sp.custom_subdomain}.{platform.domain or platform.code}",
|
|
entry_type="custom subdomain",
|
|
platform_code=platform.code,
|
|
expected_store=expected_store,
|
|
resolved_store=resolved,
|
|
passed=passed,
|
|
note="" if passed else "custom_subdomain lookup failed",
|
|
))
|
|
|
|
# ── 2. Default subdomains per platform ──
|
|
# e.g. acme.omsflow.lu — uses Store.subdomain on the platform domain.
|
|
# With fail-closed routing, this only works if StorePlatform.custom_subdomain
|
|
# matches Store.subdomain, so this check surfaces missing entries.
|
|
for store in active_stores:
|
|
if not store.subdomain:
|
|
continue
|
|
memberships = store_memberships.get(store.id, [])
|
|
for sp, platform in memberships:
|
|
if not platform.domain:
|
|
continue
|
|
# If custom_subdomain is already set to this value, it's covered
|
|
# in check #1 above — but we still check default subdomain access
|
|
# to confirm it resolves (it exercises the same code path).
|
|
# Skip if custom_subdomain == subdomain to avoid duplicate entries.
|
|
if sp.custom_subdomain and sp.custom_subdomain.lower() == store.subdomain.lower():
|
|
continue
|
|
|
|
context = {
|
|
"detection_method": "subdomain",
|
|
"subdomain": store.subdomain,
|
|
"_platform": platform,
|
|
}
|
|
resolved = StoreContextManager.get_store_from_context(db, context)
|
|
|
|
passed = resolved is not None and resolved.id == store.id
|
|
note = ""
|
|
if not passed:
|
|
note = (
|
|
f"Store '{store.subdomain}' not found or has no active "
|
|
f"membership on platform {platform.code}"
|
|
)
|
|
details.append(_entry(
|
|
domain=f"{store.subdomain}.{platform.domain}",
|
|
entry_type="default subdomain",
|
|
platform_code=platform.code,
|
|
expected_store=store,
|
|
resolved_store=resolved,
|
|
passed=passed,
|
|
note=note,
|
|
))
|
|
|
|
# ── 3. Custom domains (StoreDomain) ──
|
|
# e.g. wizatech.shop
|
|
store_domains = (
|
|
db.query(StoreDomain)
|
|
.filter(
|
|
StoreDomain.is_active.is_(True),
|
|
StoreDomain.is_verified.is_(True),
|
|
)
|
|
.all()
|
|
)
|
|
for sd in store_domains:
|
|
expected_store = sd.store
|
|
if not expected_store:
|
|
continue
|
|
|
|
platform = sd.platform
|
|
platform_code = platform.code if platform else None
|
|
|
|
context = {
|
|
"detection_method": "custom_domain",
|
|
"domain": sd.domain,
|
|
}
|
|
resolved = StoreContextManager.get_store_from_context(db, context)
|
|
|
|
passed = resolved is not None and resolved.id == expected_store.id
|
|
details.append(_entry(
|
|
domain=sd.domain,
|
|
entry_type="custom domain",
|
|
platform_code=platform_code,
|
|
expected_store=expected_store,
|
|
resolved_store=resolved,
|
|
passed=passed,
|
|
note="" if passed else "custom domain resolution failed",
|
|
))
|
|
|
|
# ── 4. Path-based routes ──
|
|
# e.g. /storefront/luxweb/, /store/luxweb/
|
|
# Path-based URLs use Store.subdomain as the path segment.
|
|
for store in active_stores:
|
|
if not store.subdomain:
|
|
continue
|
|
|
|
memberships = store_memberships.get(store.id, [])
|
|
platform_codes = sorted(p.code for _, p in memberships) if memberships else []
|
|
platform_label = ", ".join(platform_codes) if platform_codes else None
|
|
|
|
for prefix, label in [
|
|
("/storefront/", "storefront"),
|
|
("/store/", "store"),
|
|
]:
|
|
context = {
|
|
"detection_method": "path",
|
|
"subdomain": store.subdomain,
|
|
"path_prefix": f"{prefix}{store.subdomain}",
|
|
"full_prefix": prefix,
|
|
}
|
|
resolved = StoreContextManager.get_store_from_context(db, context)
|
|
|
|
passed = resolved is not None and resolved.id == store.id
|
|
details.append(_entry(
|
|
domain=f"{prefix}{store.subdomain}/",
|
|
entry_type=f"path ({label})",
|
|
platform_code=platform_label,
|
|
expected_store=store,
|
|
resolved_store=resolved,
|
|
passed=passed,
|
|
note="" if passed else f"path-based {label} resolution failed",
|
|
))
|
|
|
|
total = len(details)
|
|
passed_count = sum(1 for d in details if d["status"] == "pass")
|
|
|
|
return {
|
|
"total": total,
|
|
"passed": passed_count,
|
|
"failed": total - passed_count,
|
|
"details": details,
|
|
}
|
|
|
|
|
|
def _entry(
|
|
domain: str,
|
|
entry_type: str,
|
|
platform_code: str | None,
|
|
expected_store,
|
|
resolved_store,
|
|
passed: bool,
|
|
note: str,
|
|
) -> dict:
|
|
"""Build a single health-check result entry."""
|
|
return {
|
|
"domain": domain,
|
|
"type": entry_type,
|
|
"platform_code": platform_code,
|
|
"expected_store": (
|
|
getattr(expected_store, "store_code", expected_store.subdomain)
|
|
if expected_store else None
|
|
),
|
|
"resolved_store": (
|
|
getattr(resolved_store, "store_code", resolved_store.subdomain)
|
|
if resolved_store else None
|
|
),
|
|
"status": "pass" if passed else "fail",
|
|
"note": note,
|
|
}
|