This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
130 lines
3.5 KiB
Python
130 lines
3.5 KiB
Python
# app/modules/tenancy/schemas/vendor_domain.py
|
|
"""
|
|
Pydantic schemas for Vendor Domain operations.
|
|
|
|
Schemas include:
|
|
- VendorDomainCreate: For adding custom domains
|
|
- VendorDomainUpdate: For updating domain settings
|
|
- VendorDomainResponse: Standard domain response
|
|
- VendorDomainListResponse: Paginated domain list
|
|
- DomainVerificationInstructions: DNS verification instructions
|
|
"""
|
|
|
|
import re
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
class VendorDomainCreate(BaseModel):
|
|
"""Schema for adding a custom domain to vendor."""
|
|
|
|
domain: str = Field(
|
|
...,
|
|
description="Custom domain (e.g., myshop.com or shop.mybrand.com)",
|
|
min_length=3,
|
|
max_length=255,
|
|
)
|
|
is_primary: bool = Field(
|
|
default=False, description="Set as primary domain for the vendor"
|
|
)
|
|
|
|
@field_validator("domain")
|
|
@classmethod
|
|
def validate_domain(cls, v: str) -> str:
|
|
"""Validate and normalize domain."""
|
|
# Remove protocol if present
|
|
domain = v.replace("https://", "").replace("http://", "") # noqa: SEC-034
|
|
|
|
# Remove trailing slash
|
|
domain = domain.rstrip("/")
|
|
|
|
# Convert to lowercase
|
|
domain = domain.lower().strip()
|
|
|
|
# Basic validation
|
|
if not domain or "/" in domain:
|
|
raise ValueError("Invalid domain format")
|
|
|
|
if "." not in domain:
|
|
raise ValueError("Domain must have at least one dot")
|
|
|
|
# Check for reserved subdomains
|
|
reserved = ["www", "admin", "api", "mail", "smtp", "ftp", "cpanel", "webmail"]
|
|
first_part = domain.split(".")[0]
|
|
if first_part in reserved:
|
|
raise ValueError(
|
|
f"Domain cannot start with reserved subdomain: {first_part}"
|
|
)
|
|
|
|
# Validate domain format (basic regex)
|
|
domain_pattern = r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$"
|
|
if not re.match(domain_pattern, domain):
|
|
raise ValueError("Invalid domain format")
|
|
|
|
return domain
|
|
|
|
|
|
class VendorDomainUpdate(BaseModel):
|
|
"""Schema for updating vendor domain settings."""
|
|
|
|
is_primary: bool | None = Field(None, description="Set as primary domain")
|
|
is_active: bool | None = Field(None, description="Activate or deactivate domain")
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class VendorDomainResponse(BaseModel):
|
|
"""Standard schema for vendor domain response."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
domain: str
|
|
is_primary: bool
|
|
is_active: bool
|
|
is_verified: bool
|
|
ssl_status: str
|
|
verification_token: str | None = None
|
|
verified_at: datetime | None = None
|
|
ssl_verified_at: datetime | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class VendorDomainListResponse(BaseModel):
|
|
"""Schema for paginated vendor domain list."""
|
|
|
|
domains: list[VendorDomainResponse]
|
|
total: int
|
|
|
|
|
|
class DomainVerificationInstructions(BaseModel):
|
|
"""DNS verification instructions for domain ownership."""
|
|
|
|
domain: str
|
|
verification_token: str
|
|
instructions: dict[str, str]
|
|
txt_record: dict[str, str]
|
|
common_registrars: dict[str, str]
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class DomainVerificationResponse(BaseModel):
|
|
"""Response after domain verification."""
|
|
|
|
message: str
|
|
domain: str
|
|
verified_at: datetime
|
|
is_verified: bool
|
|
|
|
|
|
class DomainDeletionResponse(BaseModel):
|
|
"""Response after domain deletion."""
|
|
|
|
message: str
|
|
domain: str
|
|
vendor_id: int
|