Files
orion/app/api/v1/admin/vendor_domains.py
Samir Boulahtit cad862f469 refactor(api): introduce UserContext schema for API dependency injection
Replace direct User database model imports in API endpoints with UserContext
schema, following the architecture principle that API routes should not import
database models directly.

Changes:
- Create UserContext schema in models/schema/auth.py with from_user() factory
- Update app/api/deps.py to return UserContext from all auth dependencies
- Add _get_user_model() helper for functions needing User model access
- Update 58 API endpoint files to use UserContext instead of User
- Add noqa comments for 4 legitimate edge cases (enums, internal helpers)

Architecture validation: 0 errors (down from 61), 11 warnings remain

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 20:47:33 +01:00

310 lines
9.4 KiB
Python

# app/api/v1/admin/vendor_domains.py
"""
Admin endpoints for managing vendor custom domains.
Follows the architecture pattern:
- Endpoints only handle HTTP layer
- Business logic in service layer
- Domain exceptions bubble up to global handler
- Pydantic schemas for validation
"""
import logging
from fastapi import APIRouter, Body, Depends, Path
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.services.vendor_domain_service import vendor_domain_service
from app.services.vendor_service import vendor_service
from models.schema.auth import UserContext
from models.schema.vendor_domain import (
DomainDeletionResponse,
DomainVerificationInstructions,
DomainVerificationResponse,
VendorDomainCreate,
VendorDomainListResponse,
VendorDomainResponse,
VendorDomainUpdate,
)
router = APIRouter(prefix="/vendors")
logger = logging.getLogger(__name__)
@router.post("/{vendor_id}/domains", response_model=VendorDomainResponse)
def add_vendor_domain(
vendor_id: int = Path(..., description="Vendor ID", gt=0),
domain_data: VendorDomainCreate = Body(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Add a custom domain to vendor (Admin only).
This endpoint:
1. Validates the domain format
2. Checks if domain is already registered
3. Generates verification token
4. Creates domain record (unverified, inactive)
5. Returns domain with verification instructions
**Domain Examples:**
- myshop.com
- shop.mybrand.com
- customstore.net
**Next Steps:**
1. Vendor adds DNS TXT record
2. Admin clicks "Verify Domain" to confirm ownership
3. Once verified, domain can be activated
**Raises:**
- 404: Vendor not found
- 409: Domain already registered
- 422: Invalid domain format or reserved subdomain
"""
domain = vendor_domain_service.add_domain(
db=db, vendor_id=vendor_id, domain_data=domain_data
)
db.commit()
return VendorDomainResponse(
id=domain.id,
vendor_id=domain.vendor_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
verification_token=domain.verification_token,
verified_at=domain.verified_at,
ssl_verified_at=domain.ssl_verified_at,
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@router.get("/{vendor_id}/domains", response_model=VendorDomainListResponse)
def list_vendor_domains(
vendor_id: int = Path(..., description="Vendor ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
List all domains for a vendor (Admin only).
Returns domains ordered by:
1. Primary domains first
2. Creation date (newest first)
**Raises:**
- 404: Vendor not found
"""
# Verify vendor exists (raises VendorNotFoundException if not found)
vendor_service.get_vendor_by_id(db, vendor_id)
domains = vendor_domain_service.get_vendor_domains(db, vendor_id)
return VendorDomainListResponse(
domains=[
VendorDomainResponse(
id=d.id,
vendor_id=d.vendor_id,
domain=d.domain,
is_primary=d.is_primary,
is_active=d.is_active,
is_verified=d.is_verified,
ssl_status=d.ssl_status,
verification_token=d.verification_token if not d.is_verified else None,
verified_at=d.verified_at,
ssl_verified_at=d.ssl_verified_at,
created_at=d.created_at,
updated_at=d.updated_at,
)
for d in domains
],
total=len(domains),
)
@router.get("/domains/{domain_id}", response_model=VendorDomainResponse)
def get_domain_details(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get detailed information about a specific domain (Admin only).
**Raises:**
- 404: Domain not found
"""
domain = vendor_domain_service.get_domain_by_id(db, domain_id)
return VendorDomainResponse(
id=domain.id,
vendor_id=domain.vendor_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
verification_token=(
domain.verification_token if not domain.is_verified else None
),
verified_at=domain.verified_at,
ssl_verified_at=domain.ssl_verified_at,
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@router.put("/domains/{domain_id}", response_model=VendorDomainResponse)
def update_vendor_domain(
domain_id: int = Path(..., description="Domain ID", gt=0),
domain_update: VendorDomainUpdate = Body(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Update domain settings (Admin only).
**Can update:**
- `is_primary`: Set as primary domain for vendor
- `is_active`: Activate or deactivate domain
**Important:**
- Cannot activate unverified domains
- Setting a domain as primary will unset other primary domains
- Cannot modify domain name (delete and recreate instead)
**Raises:**
- 404: Domain not found
- 400: Cannot activate unverified domain
"""
domain = vendor_domain_service.update_domain(
db=db, domain_id=domain_id, domain_update=domain_update
)
db.commit()
return VendorDomainResponse(
id=domain.id,
vendor_id=domain.vendor_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
verification_token=None, # Don't expose token after updates
verified_at=domain.verified_at,
ssl_verified_at=domain.ssl_verified_at,
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@router.delete("/domains/{domain_id}", response_model=DomainDeletionResponse)
def delete_vendor_domain(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Delete a custom domain (Admin only).
**Warning:** This is permanent and cannot be undone.
**Raises:**
- 404: Domain not found
"""
# Get domain details before deletion
domain = vendor_domain_service.get_domain_by_id(db, domain_id)
vendor_id = domain.vendor_id
domain_name = domain.domain
# Delete domain
message = vendor_domain_service.delete_domain(db, domain_id)
db.commit()
return DomainDeletionResponse(
message=message, domain=domain_name, vendor_id=vendor_id
)
@router.post("/domains/{domain_id}/verify", response_model=DomainVerificationResponse)
def verify_domain_ownership(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Verify domain ownership via DNS TXT record (Admin only).
**Verification Process:**
1. Queries DNS for TXT record: `_wizamart-verify.{domain}`
2. Checks if verification token matches
3. If found, marks domain as verified
**Requirements:**
- Vendor must have added TXT record to their DNS
- DNS propagation may take 5-15 minutes
- Record format: `_wizamart-verify.domain.com` TXT `{token}`
**After verification:**
- Domain can be activated
- Domain will be available for routing
**Raises:**
- 404: Domain not found
- 400: Already verified, or verification failed
- 502: DNS query failed
"""
domain, message = vendor_domain_service.verify_domain(db, domain_id)
db.commit()
return DomainVerificationResponse(
message=message,
domain=domain.domain,
verified_at=domain.verified_at,
is_verified=domain.is_verified,
)
@router.get(
"/domains/{domain_id}/verification-instructions",
response_model=DomainVerificationInstructions,
)
def get_domain_verification_instructions(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get DNS verification instructions for domain (Admin only).
Returns step-by-step instructions for:
1. Where to add DNS records
2. What TXT record to create
3. Links to common registrars
4. Verification token
**Use this endpoint to:**
- Show vendors how to verify their domain
- Get the exact TXT record values
- Access registrar links
**Raises:**
- 404: Domain not found
"""
instructions = vendor_domain_service.get_verification_instructions(db, domain_id)
return DomainVerificationInstructions(
domain=instructions["domain"],
verification_token=instructions["verification_token"],
instructions=instructions["instructions"],
txt_record=instructions["txt_record"],
common_registrars=instructions["common_registrars"],
)