pounce/backend/app/api/drops.py
Yves Gugger c5abba5d2f
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
feat: award-winning hunt tabs + .ch/.li zone file drops
2025-12-15 21:45:42 +01:00

149 lines
4.6 KiB
Python

"""
Drops API - Zone File Analysis Endpoints
=========================================
API endpoints for accessing freshly dropped .ch and .li domains.
"""
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"])
# ============================================================================
# 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 (ch or li)"),
days: int = Query(7, ge=1, le=30, description="Days to look back"),
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 .ch and .li zone files.
Domains are detected by comparing daily zone file snapshots.
Only available for authenticated users.
"""
if tld and tld not in ["ch", "li"]:
raise HTTPException(status_code=400, detail="TLD must be 'ch' or 'li'")
try:
result = await get_dropped_domains(
db=db,
tld=tld,
days=days,
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 ["ch", "li"]:
raise HTTPException(status_code=400, detail="TLD must be 'ch' or 'li'")
# Run sync in background
service = ZoneFileService()
async def run_sync():
async for session in get_db():
try:
await service.run_daily_sync(session, tld)
except Exception as e:
print(f"Zone sync failed for .{tld}: {e}")
break
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": [
{
"tld": "ch",
"name": "Switzerland",
"flag": "🇨🇭",
"registry": "Switch"
},
{
"tld": "li",
"name": "Liechtenstein",
"flag": "🇱🇮",
"registry": "Switch"
}
]
}