""" Drops API - Zone File Analysis Endpoints ========================================= API endpoints for accessing freshly dropped domains from: - Switch.ch zone files (.ch, .li) - ICANN CZDS zone files (.com, .net, .org, .xyz, .info, .dev, .app, .online) """ from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.api.deps import get_current_user from app.services.zone_file import ( ZoneFileService, get_dropped_domains, get_zone_stats, ) router = APIRouter(prefix="/drops", tags=["drops"]) # All supported TLDs SWITCH_TLDS = ["ch", "li"] CZDS_TLDS = ["xyz", "org", "online", "info", "dev", "app"] # Approved CZDS_PENDING = ["com", "net", "club", "biz"] # Pending approval ALL_TLDS = SWITCH_TLDS + CZDS_TLDS # ============================================================================ # PUBLIC ENDPOINTS (for stats) # ============================================================================ @router.get("/stats") async def api_get_zone_stats( db: AsyncSession = Depends(get_db) ): """ Get zone file statistics. Returns domain counts and last sync times for .ch and .li. """ try: stats = await get_zone_stats(db) return stats except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # AUTHENTICATED ENDPOINTS # ============================================================================ @router.get("") async def api_get_drops( tld: Optional[str] = Query(None, description="Filter by TLD"), hours: int = Query(24, ge=1, le=48, description="Hours to look back (max 48h, we only store 48h)"), min_length: Optional[int] = Query(None, ge=1, le=63, description="Minimum domain length"), max_length: Optional[int] = Query(None, ge=1, le=63, description="Maximum domain length"), exclude_numeric: bool = Query(False, description="Exclude numeric-only domains"), exclude_hyphen: bool = Query(False, description="Exclude domains with hyphens"), keyword: Optional[str] = Query(None, description="Search keyword"), limit: int = Query(50, ge=1, le=200, description="Results per page"), offset: int = Query(0, ge=0, description="Offset for pagination"), db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user) ): """ Get recently dropped domains from zone files. Supports: - Switch.ch zones: .ch, .li - ICANN CZDS zones: .xyz, .org, .online, .info, .dev, .app Domains are detected by comparing daily zone file snapshots. Only available for authenticated users. """ if tld and tld not in ALL_TLDS: raise HTTPException( status_code=400, detail=f"Unsupported TLD. Supported: {', '.join(ALL_TLDS)}" ) try: result = await get_dropped_domains( db=db, tld=tld, hours=hours, min_length=min_length, max_length=max_length, exclude_numeric=exclude_numeric, exclude_hyphen=exclude_hyphen, keyword=keyword, limit=limit, offset=offset ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/sync/{tld}") async def api_trigger_sync( tld: str, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user) ): """ Trigger a manual zone file sync for a specific TLD. Only available for admin users. This is normally run automatically by the scheduler. """ # Check if user is admin if not getattr(current_user, 'is_admin', False): raise HTTPException(status_code=403, detail="Admin access required") if tld not in ALL_TLDS: raise HTTPException( status_code=400, detail=f"Unsupported TLD. Supported: {', '.join(ALL_TLDS)}" ) async def run_sync(): from app.database import AsyncSessionLocal async with AsyncSessionLocal() as session: try: if tld in SWITCH_TLDS: # Use Switch.ch zone transfer service = ZoneFileService() await service.run_daily_sync(session, tld) else: # Use ICANN CZDS from app.services.czds_client import CZDSClient client = CZDSClient() await client.sync_zone(session, tld) except Exception as e: print(f"Zone sync failed for .{tld}: {e}") background_tasks.add_task(run_sync) return {"status": "sync_started", "tld": tld} # ============================================================================ # HELPER ENDPOINTS # ============================================================================ @router.get("/tlds") async def api_get_supported_tlds(): """ Get list of supported TLDs for zone file analysis. """ return { "tlds": [ # Switch.ch zones {"tld": "ch", "name": "Switzerland", "flag": "🇨🇭", "registry": "Switch", "source": "switch"}, {"tld": "li", "name": "Liechtenstein", "flag": "🇱🇮", "registry": "Switch", "source": "switch"}, # ICANN CZDS zones (approved) {"tld": "xyz", "name": "XYZ", "flag": "🌐", "registry": "XYZ.COM LLC", "source": "czds"}, {"tld": "org", "name": "Organization", "flag": "đŸ›ī¸", "registry": "PIR", "source": "czds"}, {"tld": "online", "name": "Online", "flag": "đŸ’ģ", "registry": "Radix", "source": "czds"}, {"tld": "info", "name": "Information", "flag": "â„šī¸", "registry": "Afilias", "source": "czds"}, {"tld": "dev", "name": "Developer", "flag": "👨‍đŸ’ģ", "registry": "Google", "source": "czds"}, {"tld": "app", "name": "Application", "flag": "📱", "registry": "Google", "source": "czds"}, ], "pending": [ # CZDS pending approval {"tld": "com", "name": "Commercial", "flag": "đŸĸ", "registry": "Verisign", "source": "czds"}, {"tld": "net", "name": "Network", "flag": "🌐", "registry": "Verisign", "source": "czds"}, {"tld": "club", "name": "Club", "flag": "🎉", "registry": "GoDaddy", "source": "czds"}, {"tld": "biz", "name": "Business", "flag": "đŸ’ŧ", "registry": "GoDaddy", "source": "czds"}, ] }