feat(middleware): harden routing with fail-closed policy, custom subdomain management, and perf fixes
- 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>
This commit is contained in:
@@ -231,15 +231,17 @@ class MerchantStoreService:
|
||||
|
||||
# Assign to platforms if provided
|
||||
platform_ids = store_data.get("platform_ids", [])
|
||||
store_platforms = []
|
||||
for pid in platform_ids:
|
||||
platform = db.query(Platform).filter(Platform.id == pid).first()
|
||||
if platform:
|
||||
sp = StorePlatform(
|
||||
store_platforms.append(StorePlatform(
|
||||
store_id=store.id,
|
||||
platform_id=pid,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(sp)
|
||||
))
|
||||
if store_platforms:
|
||||
db.add_all(store_platforms)
|
||||
|
||||
db.flush()
|
||||
db.refresh(store)
|
||||
|
||||
170
app/modules/tenancy/services/store_subdomain_service.py
Normal file
170
app/modules/tenancy/services/store_subdomain_service.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# app/modules/tenancy/services/store_subdomain_service.py
|
||||
"""
|
||||
Service for managing StorePlatform custom subdomains.
|
||||
|
||||
Handles validation (format, uniqueness) and CRUD operations on
|
||||
the custom_subdomain field of StorePlatform entries.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions.base import (
|
||||
ConflictException,
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
)
|
||||
from app.modules.tenancy.models import Platform, Store
|
||||
from app.modules.tenancy.models.store_platform import StorePlatform
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Subdomain rules: lowercase alphanumeric + hyphens, 3-63 chars, no leading/trailing hyphen
|
||||
_SUBDOMAIN_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])?$")
|
||||
|
||||
|
||||
class StoreSubdomainService:
|
||||
"""Manage custom subdomains on StorePlatform entries."""
|
||||
|
||||
def get_custom_subdomains(self, db: Session, store_id: int) -> list[dict]:
|
||||
"""
|
||||
List all platform memberships for a store with their custom subdomains.
|
||||
|
||||
Returns a list of dicts with platform info and custom_subdomain (may be None).
|
||||
"""
|
||||
store = db.query(Store).filter(Store.id == store_id).first()
|
||||
if not store:
|
||||
raise ResourceNotFoundException("Store", str(store_id))
|
||||
|
||||
memberships = (
|
||||
db.query(StorePlatform)
|
||||
.filter(
|
||||
StorePlatform.store_id == store_id,
|
||||
StorePlatform.is_active.is_(True),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
results = []
|
||||
for sp in memberships:
|
||||
platform = db.query(Platform).filter(Platform.id == sp.platform_id).first()
|
||||
if not platform:
|
||||
continue
|
||||
results.append({
|
||||
"store_platform_id": sp.id,
|
||||
"platform_id": sp.platform_id,
|
||||
"platform_code": platform.code,
|
||||
"platform_name": platform.name,
|
||||
"platform_domain": platform.domain,
|
||||
"custom_subdomain": sp.custom_subdomain,
|
||||
"default_subdomain": store.subdomain,
|
||||
"full_url": (
|
||||
f"{sp.custom_subdomain}.{platform.domain}"
|
||||
if sp.custom_subdomain and platform.domain
|
||||
else None
|
||||
),
|
||||
"default_url": (
|
||||
f"{store.subdomain}.{platform.domain}"
|
||||
if store.subdomain and platform.domain
|
||||
else None
|
||||
),
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def set_custom_subdomain(
|
||||
self, db: Session, store_id: int, platform_id: int, subdomain: str
|
||||
) -> StorePlatform:
|
||||
"""
|
||||
Set or update the custom_subdomain for a store on a specific platform.
|
||||
|
||||
Validates:
|
||||
- Subdomain format (lowercase, alphanumeric + hyphens)
|
||||
- Uniqueness on the platform (no other store claims it)
|
||||
- StorePlatform membership exists and is active
|
||||
"""
|
||||
subdomain = subdomain.strip().lower()
|
||||
|
||||
# Validate format
|
||||
if not _SUBDOMAIN_RE.match(subdomain):
|
||||
raise ValidationException(
|
||||
"Must be 3-63 characters, lowercase alphanumeric and hyphens, "
|
||||
"cannot start or end with a hyphen.",
|
||||
field="custom_subdomain",
|
||||
)
|
||||
|
||||
# Find the membership
|
||||
sp = (
|
||||
db.query(StorePlatform)
|
||||
.filter(
|
||||
StorePlatform.store_id == store_id,
|
||||
StorePlatform.platform_id == platform_id,
|
||||
StorePlatform.is_active.is_(True),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not sp:
|
||||
raise ResourceNotFoundException(
|
||||
"StorePlatform",
|
||||
f"store_id={store_id}, platform_id={platform_id}",
|
||||
)
|
||||
|
||||
# Check uniqueness on this platform (exclude current entry)
|
||||
existing = (
|
||||
db.query(StorePlatform)
|
||||
.filter(
|
||||
func.lower(StorePlatform.custom_subdomain) == subdomain,
|
||||
StorePlatform.platform_id == platform_id,
|
||||
StorePlatform.id != sp.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
raise ConflictException(
|
||||
f"Subdomain '{subdomain}' is already claimed by another store "
|
||||
f"on this platform."
|
||||
)
|
||||
|
||||
sp.custom_subdomain = subdomain
|
||||
db.flush()
|
||||
|
||||
logger.info(
|
||||
f"Set custom_subdomain='{subdomain}' for store_id={store_id} "
|
||||
f"on platform_id={platform_id}"
|
||||
)
|
||||
return sp
|
||||
|
||||
def clear_custom_subdomain(
|
||||
self, db: Session, store_id: int, platform_id: int
|
||||
) -> StorePlatform:
|
||||
"""Clear the custom_subdomain for a store on a specific platform."""
|
||||
sp = (
|
||||
db.query(StorePlatform)
|
||||
.filter(
|
||||
StorePlatform.store_id == store_id,
|
||||
StorePlatform.platform_id == platform_id,
|
||||
StorePlatform.is_active.is_(True),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not sp:
|
||||
raise ResourceNotFoundException(
|
||||
"StorePlatform",
|
||||
f"store_id={store_id}, platform_id={platform_id}",
|
||||
)
|
||||
|
||||
old_value = sp.custom_subdomain
|
||||
sp.custom_subdomain = None
|
||||
db.flush()
|
||||
|
||||
logger.info(
|
||||
f"Cleared custom_subdomain (was '{old_value}') for store_id={store_id} "
|
||||
f"on platform_id={platform_id}"
|
||||
)
|
||||
return sp
|
||||
|
||||
|
||||
store_subdomain_service = StoreSubdomainService()
|
||||
Reference in New Issue
Block a user