Files
orion/app/modules/tenancy/services/store_subdomain_service.py
Samir Boulahtit 540205402f
Some checks failed
CI / pytest (push) Waiting to run
CI / ruff (push) Successful in 12s
CI / validate (push) Successful in 26s
CI / dependency-scanning (push) Successful in 31s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
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>
2026-03-15 18:13:01 +01:00

171 lines
5.4 KiB
Python

# 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()