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:
@@ -27,6 +27,7 @@ from .admin_platform_users import admin_platform_users_router
|
||||
from .admin_platforms import admin_platforms_router
|
||||
from .admin_store_domains import admin_store_domains_router
|
||||
from .admin_store_roles import admin_store_roles_router
|
||||
from .admin_store_subdomains import admin_store_subdomains_router
|
||||
from .admin_stores import admin_stores_router
|
||||
from .admin_users import admin_users_router
|
||||
from .user_account import admin_account_router
|
||||
@@ -42,6 +43,7 @@ router.include_router(admin_merchants_router, tags=["admin-merchants"])
|
||||
router.include_router(admin_platforms_router, tags=["admin-platforms"])
|
||||
router.include_router(admin_stores_router, tags=["admin-stores"])
|
||||
router.include_router(admin_store_domains_router, tags=["admin-store-domains"])
|
||||
router.include_router(admin_store_subdomains_router, tags=["admin-store-subdomains"])
|
||||
router.include_router(admin_store_roles_router, tags=["admin-store-roles"])
|
||||
router.include_router(admin_merchant_domains_router, tags=["admin-merchant-domains"])
|
||||
router.include_router(admin_modules_router, tags=["admin-modules"])
|
||||
|
||||
137
app/modules/tenancy/routes/api/admin_store_subdomains.py
Normal file
137
app/modules/tenancy/routes/api/admin_store_subdomains.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# app/modules/tenancy/routes/api/admin_store_subdomains.py
|
||||
"""
|
||||
Admin endpoints for managing store custom subdomains (per-platform).
|
||||
|
||||
Each store can have a custom subdomain on each platform it belongs to.
|
||||
For example, store "WizaTech" on the loyalty platform could have
|
||||
custom_subdomain="wizatech-rewards" → wizatech-rewards.rewardflow.lu
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.services.store_subdomain_service import store_subdomain_service
|
||||
|
||||
admin_store_subdomains_router = APIRouter(prefix="/stores")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Schemas ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class CustomSubdomainEntry(BaseModel):
|
||||
store_platform_id: int
|
||||
platform_id: int
|
||||
platform_code: str
|
||||
platform_name: str
|
||||
platform_domain: str | None
|
||||
custom_subdomain: str | None
|
||||
default_subdomain: str | None
|
||||
full_url: str | None
|
||||
default_url: str | None
|
||||
|
||||
|
||||
class CustomSubdomainListResponse(BaseModel):
|
||||
subdomains: list[CustomSubdomainEntry]
|
||||
total: int
|
||||
|
||||
|
||||
class SetCustomSubdomainRequest(BaseModel):
|
||||
subdomain: str = Field(
|
||||
...,
|
||||
min_length=3,
|
||||
max_length=63,
|
||||
description="Custom subdomain (lowercase, alphanumeric + hyphens)",
|
||||
)
|
||||
|
||||
|
||||
class CustomSubdomainUpdateResponse(BaseModel):
|
||||
message: str
|
||||
platform_id: int
|
||||
custom_subdomain: str | None
|
||||
|
||||
|
||||
# ── Endpoints ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@admin_store_subdomains_router.get(
|
||||
"/{store_id}/custom-subdomains",
|
||||
response_model=CustomSubdomainListResponse,
|
||||
)
|
||||
def list_custom_subdomains(
|
||||
store_id: int = Path(..., description="Store ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
List all platform memberships with their custom subdomains.
|
||||
|
||||
Returns one entry per active platform the store belongs to,
|
||||
showing the custom_subdomain (if set) and the default subdomain.
|
||||
"""
|
||||
entries = store_subdomain_service.get_custom_subdomains(db, store_id)
|
||||
return CustomSubdomainListResponse(
|
||||
subdomains=[CustomSubdomainEntry(**e) for e in entries],
|
||||
total=len(entries),
|
||||
)
|
||||
|
||||
|
||||
@admin_store_subdomains_router.put(
|
||||
"/{store_id}/custom-subdomains/{platform_id}",
|
||||
response_model=CustomSubdomainUpdateResponse,
|
||||
)
|
||||
def set_custom_subdomain(
|
||||
store_id: int = Path(..., description="Store ID", gt=0),
|
||||
platform_id: int = Path(..., description="Platform ID", gt=0),
|
||||
payload: SetCustomSubdomainRequest = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Set or update the custom subdomain for a store on a specific platform.
|
||||
|
||||
The subdomain must be unique on the platform (no other store can claim it).
|
||||
Format: lowercase alphanumeric + hyphens, 3-63 characters.
|
||||
"""
|
||||
sp = store_subdomain_service.set_custom_subdomain(
|
||||
db, store_id, platform_id, payload.subdomain
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return CustomSubdomainUpdateResponse(
|
||||
message=f"Custom subdomain set to '{sp.custom_subdomain}'",
|
||||
platform_id=platform_id,
|
||||
custom_subdomain=sp.custom_subdomain,
|
||||
)
|
||||
|
||||
|
||||
@admin_store_subdomains_router.delete(
|
||||
"/{store_id}/custom-subdomains/{platform_id}",
|
||||
response_model=CustomSubdomainUpdateResponse,
|
||||
)
|
||||
def clear_custom_subdomain(
|
||||
store_id: int = Path(..., description="Store ID", gt=0),
|
||||
platform_id: int = Path(..., description="Platform ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Clear the custom subdomain for a store on a specific platform.
|
||||
|
||||
The store will still be accessible via its default subdomain
|
||||
(Store.subdomain + platform domain).
|
||||
"""
|
||||
store_subdomain_service.clear_custom_subdomain(db, store_id, platform_id)
|
||||
db.commit()
|
||||
|
||||
return CustomSubdomainUpdateResponse(
|
||||
message="Custom subdomain cleared",
|
||||
platform_id=platform_id,
|
||||
custom_subdomain=None,
|
||||
)
|
||||
Reference in New Issue
Block a user