Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
178 lines
6.6 KiB
Python
178 lines
6.6 KiB
Python
"""
|
||
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"},
|
||
]
|
||
}
|