fix(auth): CiroMarket kategori 401 kalıcı düzeltme — router-based auth

Ne yaptık:
- PUBLIC_PATHS whitelist tamamen kaldırıldı
- protected_router = APIRouter(dependencies=[Depends(verify_api_key)]) eklendi
- 33 korunan route @protected_router.*'a taşındı
- 6 public route (/health, /, category-tree×4) @app.*'de kaldı
- verify_api_key sadeleşti: request.url.path artık kontrol edilmiyor
- test_auth_routing.py: regresyon guard — PUBLIC_PATHS geri gelirse CI fail eder

Neden yaptık:
- PUBLIC_PATHS her deploy/merge'de kayboluyordu → tekrarlayan 401
- Yapısal çözüm: ayrım kod mimarisinde, whitelist config'de değil
This commit is contained in:
furkanyigit34
2026-07-22 00:10:22 +03:00
parent 73d6b3e9c0
commit eba9709962
4 changed files with 349 additions and 342 deletions

View File

@@ -1,7 +1,7 @@
"""
FastAPI Backend for Trendyol Admin Panel
"""
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks, Request, Security
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks, Request, Security, APIRouter
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from fastapi.security import APIKeyHeader
@@ -24,7 +24,6 @@ from threading import Lock
import os
from database import SessionLocal, Category, Snapshot, Report, EnrichmentError, ReportQueue, init_db
from google_trends_helper import estimate_traffic_sources, fetch_google_trends
from logging_config import setup_logging, get_logger, set_correlation_id, set_report_id, log_timing
# Initialize logging first, then database
@@ -51,21 +50,10 @@ if not API_KEY:
warnings.warn("API_KEY env var not set! API authentication disabled in development.")
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
# Paths that do not require API key authentication
PUBLIC_PATHS = {"/health", "/api/category-tree", "/api/category-tree/roots", "/api/category-tree/search"}
async def verify_api_key(request: Request, api_key: Optional[str] = Security(API_KEY_HEADER)):
"""
Global dependency that validates X-API-Key header.
Skips authentication for public paths (health check, docs).
"""
if request.url.path in PUBLIC_PATHS:
return
if not API_KEY:
# API_KEY env var not set — authentication disabled
return
async def verify_api_key(api_key: Optional[str] = Security(API_KEY_HEADER)):
"""Validates X-API-Key header. Public routes are defined on app directly."""
if not api_key or api_key != API_KEY:
raise HTTPException(status_code=404, detail="Not Found")
raise HTTPException(status_code=401, detail="Invalid or missing API key")
# GS1 Barcode Prefix to Country Mapping (EAN-13 / EAN-8)
# Source: https://www.gs1.org/standards/id-keys/company-prefix
@@ -289,9 +277,11 @@ def get_country_from_barcode(barcode: str) -> str:
app = FastAPI(
title="Trendyol Admin API",
version="1.0.0",
dependencies=[Depends(verify_api_key)]
)
# All non-public routes attach here. Auth is enforced once at the router level.
protected_router = APIRouter(dependencies=[Depends(verify_api_key)])
# Base directory for resolving relative paths
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -309,6 +299,8 @@ allowed_origins = []
if os.getenv("ENV", "development") == "production":
allowed_origins = [
"https://trendyol.194.187.253.230.sslip.io",
"https://cironet.com.tr",
"https://test.cironet.com.tr",
]
else:
allowed_origins = [
@@ -496,24 +488,10 @@ class BoundedCache:
# Bounded caches for external API aggregations (prevent memory leaks)
reviews_cache = BoundedCache(maxsize=100, ttl=3600)
social_proof_cache = BoundedCache(maxsize=100, ttl=3600)
product_lookup_cache = BoundedCache(maxsize=500, ttl=3600)
scraping_progress = BoundedCache(maxsize=50, ttl=7200)
dashboard_cache = BoundedCache(maxsize=50, ttl=3600)
enrichment_progress = BoundedCache(maxsize=50, ttl=7200)
product_lookup_cache = BoundedCache(maxsize=500, ttl=3600)
# IP rate limiter for public product lookup
_product_lookup_ip_cache: dict = {}
_product_lookup_ip_lock = Lock()
_PRODUCT_LOOKUP_MAX = 10 # per IP per minute
_TRENDYOL_URL_RE = re.compile(r'trendyol\.com/.*?-p-(\d+)', re.IGNORECASE)
def _check_ip_rate_limit(ip: str) -> bool:
now = time.time(); cutoff = now - 60.0
with _product_lookup_ip_lock:
ts = [t for t in _product_lookup_ip_cache.get(ip, []) if t > cutoff]
if len(ts) >= _PRODUCT_LOOKUP_MAX:
_product_lookup_ip_cache[ip] = ts; return False
ts.append(now); _product_lookup_ip_cache[ip] = ts; return True
# DISABLED: Questions, similar products, and followers features removed per user request
# questions_cache = {}
@@ -640,6 +618,27 @@ class _RateLimiter:
_trendyol_limiter = _RateLimiter(rate_per_sec=1.5) # ~0.67s between requests (safe for Trendyol rate limits)
# URL pattern for extracting product ID from Trendyol URLs
_TRENDYOL_URL_RE = re.compile(r'trendyol\.com/.*?-p-(\d+)', re.IGNORECASE)
# IP-based rate limiting for /api/product/lookup (10 req/IP/min)
_product_lookup_ip_cache: dict = {}
_product_lookup_ip_lock = Lock()
_PRODUCT_LOOKUP_MAX = 10 # per IP per minute
def _check_ip_rate_limit(ip: str) -> bool:
now = time.time()
cutoff = now - 60.0
with _product_lookup_ip_lock:
ts = [t for t in _product_lookup_ip_cache.get(ip, []) if t > cutoff]
if len(ts) >= _PRODUCT_LOOKUP_MAX:
_product_lookup_ip_cache[ip] = ts
return False
ts.append(now)
_product_lookup_ip_cache[ip] = ts
return True
# Circuit Breaker for Social Proof endpoint
class _CircuitBreaker:
@@ -934,6 +933,54 @@ def _parse_social_count(count_str: str) -> int:
return 0
@protected_router.get("/api/product/lookup")
def product_lookup(url: str, request: Request):
"""
Landing page lead magnet: look up social proof metrics for a single Trendyol product URL.
Rate limited per IP (10 req/min). Results cached for 1 hour.
"""
# IP rate limiting
ip = request.headers.get("X-Forwarded-For", request.client.host or "unknown").split(",")[0].strip()
if not _check_ip_rate_limit(ip):
raise HTTPException(status_code=429, detail="Çok fazla istek. Bir dakika sonra tekrar deneyin.")
# URL validation
if not url or "trendyol.com" not in url.lower():
raise HTTPException(status_code=400, detail="Geçerli bir Trendyol URL'si girin.")
m = _TRENDYOL_URL_RE.search(url)
if not m:
raise HTTPException(status_code=400, detail="URL'den ürün ID'si alınamadı. Trendyol ürün sayfası URL'si girin.")
product_id = int(m.group(1))
# Cache hit
cached = product_lookup_cache.get(str(product_id))
if cached:
return {"source": "cache", "productId": product_id, **cached}
# Circuit breaker check
if _social_proof_breaker.is_open():
raise HTTPException(status_code=503, detail="Servis geçici olarak kullanılamıyor. Lütfen daha sonra tekrar deneyin.")
# Fetch social proof
data = fetch_social_proof([product_id])
if not data:
_social_proof_breaker.record_failure()
raise HTTPException(status_code=404, detail="Bu ürün için veri bulunamadı.")
items = data.get("result") or []
pd_item = next((it for it in items if it.get("contentId") == product_id), None)
if not pd_item:
raise HTTPException(status_code=404, detail="Sosyal kanıt verisi bulunamadı.")
result = {k: pd_item.get(k, 0) for k in ["orderCount", "favoriteCount", "pageViewCount", "basketCount"]}
product_lookup_cache.set(str(product_id), result)
_social_proof_breaker.record_success()
return {"source": "live", "productId": product_id, **result}
def fetch_merchant_questions(product_id: int, page: int = 0, page_size: int = 4):
"""Satıcı sorularını çeker"""
url = f"https://apigw.trendyol.com/discovery-pdp-websfxmerchantquestions-santral/{product_id}/questions/answered/filter"
@@ -1058,7 +1105,7 @@ def root():
# Get all categories
@app.get("/categories", response_model=List[CategoryResponse])
@protected_router.get("/categories", response_model=List[CategoryResponse])
def get_categories(db: Session = Depends(get_db), skip: int = 0, limit: int = 200):
"""Get all categories with pagination (OPTIMIZED: single query for children counts)"""
categories = db.query(Category).offset(skip).limit(limit).all()
@@ -1093,7 +1140,7 @@ def get_categories(db: Session = Depends(get_db), skip: int = 0, limit: int = 20
# Get main categories (no parent)
@app.get("/categories/main", response_model=List[CategoryResponse])
@protected_router.get("/categories/main", response_model=List[CategoryResponse])
def get_main_categories(db: Session = Depends(get_db)):
"""Get only main categories (parent_id is NULL) - OPTIMIZED"""
categories = db.query(Category).filter(Category.parent_id == None).all()
@@ -1128,7 +1175,7 @@ def get_main_categories(db: Session = Depends(get_db)):
# Get category by ID
@app.get("/categories/{category_id}", response_model=CategoryResponse)
@protected_router.get("/categories/{category_id}", response_model=CategoryResponse)
def get_category(category_id: int, db: Session = Depends(get_db)):
"""Get a specific category by ID"""
category = db.query(Category).filter(Category.id == category_id).first()
@@ -1154,7 +1201,7 @@ def get_category(category_id: int, db: Session = Depends(get_db)):
# Get category children (subcategories)
@app.get("/categories/{category_id}/children", response_model=List[CategoryResponse])
@protected_router.get("/categories/{category_id}/children", response_model=List[CategoryResponse])
def get_category_children(category_id: int, db: Session = Depends(get_db)):
"""Get all subcategories of a category - OPTIMIZED"""
# Verify parent exists
@@ -1195,7 +1242,7 @@ def get_category_children(category_id: int, db: Session = Depends(get_db)):
# Create new category
@app.post("/categories", response_model=CategoryResponse, status_code=201)
@protected_router.post("/categories", response_model=CategoryResponse, status_code=201)
def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
"""Create a new category"""
# Verify parent exists if parent_id provided
@@ -1234,7 +1281,7 @@ def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
# Update category
@app.put("/categories/{category_id}", response_model=CategoryResponse)
@protected_router.put("/categories/{category_id}", response_model=CategoryResponse)
def update_category(category_id: int, category: CategoryUpdate, db: Session = Depends(get_db)):
"""Update an existing category"""
db_category = db.query(Category).filter(Category.id == category_id).first()
@@ -1281,7 +1328,7 @@ def update_category(category_id: int, category: CategoryUpdate, db: Session = De
# Delete category
@app.delete("/categories/{category_id}")
@protected_router.delete("/categories/{category_id}")
def delete_category(category_id: int, db: Session = Depends(get_db)):
"""Delete a category"""
db_category = db.query(Category).filter(Category.id == category_id).first()
@@ -1314,7 +1361,7 @@ class BulkCategoryImport(BaseModel):
categories: List[BulkCategoryItem]
clear_existing: bool = False
@app.post("/categories/bulk-import")
@protected_router.post("/categories/bulk-import")
def bulk_import_categories(data: BulkCategoryImport, db: Session = Depends(get_db)):
"""Bulk import categories with hierarchy support.
Categories are processed in order: parent categories should come before children.
@@ -1364,7 +1411,7 @@ def bulk_import_categories(data: BulkCategoryImport, db: Session = Depends(get_d
}
@app.post("/categories/seed-from-json")
@protected_router.post("/categories/seed-from-json")
def seed_from_json_endpoint(clear_existing: bool = True):
"""Seed categories from trendyol_categories.json file"""
from category_seeder import seed_from_json
@@ -1378,7 +1425,7 @@ def seed_from_json_endpoint(clear_existing: bool = True):
# Get all snapshots
@app.get("/snapshots", response_model=List[SnapshotResponse])
@protected_router.get("/snapshots", response_model=List[SnapshotResponse])
def get_snapshots(db: Session = Depends(get_db), skip: int = 0, limit: int = 100):
"""Get all snapshots with pagination"""
snapshots = db.query(Snapshot).offset(skip).limit(limit).all()
@@ -1386,7 +1433,7 @@ def get_snapshots(db: Session = Depends(get_db), skip: int = 0, limit: int = 100
# Get snapshots for a category
@app.get("/categories/{category_id}/snapshots", response_model=List[SnapshotResponse])
@protected_router.get("/categories/{category_id}/snapshots", response_model=List[SnapshotResponse])
def get_category_snapshots(category_id: int, db: Session = Depends(get_db)):
"""Get all snapshots for a specific category"""
# Verify category exists
@@ -1399,7 +1446,7 @@ def get_category_snapshots(category_id: int, db: Session = Depends(get_db)):
# Get products for a category from JSON file
@app.get("/categories/{category_id}/products")
@protected_router.get("/categories/{category_id}/products")
def get_category_products(category_id: int, db: Session = Depends(get_db)):
"""Get products from category JSON file"""
import json
@@ -1476,7 +1523,7 @@ def collect_scrapable_categories(db: Session, category_ids: list) -> list:
# Scraping endpoint
@app.post("/api/scrape/category/{category_id}")
@protected_router.post("/api/scrape/category/{category_id}")
def scrape_category_data(category_id: int, db: Session = Depends(get_db)):
"""
Scrape all subcategories of a main category
@@ -1565,7 +1612,7 @@ class ReportResponse(BaseModel):
# Get all reports
@app.get("/api/reports", response_model=List[ReportResponse])
@protected_router.get("/api/reports", response_model=List[ReportResponse])
def get_reports(db: Session = Depends(get_db)):
"""Get all saved reports"""
reports = db.query(Report).order_by(Report.created_at.desc()).all()
@@ -1593,7 +1640,7 @@ def get_reports(db: Session = Depends(get_db)):
# Get single report
@app.get("/api/reports/create")
@protected_router.get("/api/reports/create")
async def create_report(
name: str,
category_id: int,
@@ -1911,7 +1958,7 @@ class QueueSubmitRequest(BaseModel):
name: str
category_id: int
@app.post("/api/queue/submit", status_code=202)
@protected_router.post("/api/queue/submit", status_code=202)
def submit_to_queue(req: QueueSubmitRequest, db: Session = Depends(get_db)):
"""Submit a report generation task to the queue. Returns 202 Accepted."""
# Validate category exists
@@ -1940,7 +1987,7 @@ def submit_to_queue(req: QueueSubmitRequest, db: Session = Depends(get_db)):
return {"queue_id": queue_item.id, "status": "PENDING", "position": position}
@app.get("/api/queue/{queue_id}/status")
@protected_router.get("/api/queue/{queue_id}/status")
def get_queue_status(queue_id: int, db: Session = Depends(get_db)):
"""Get the status of a queued report task."""
item = db.query(ReportQueue).filter(ReportQueue.id == queue_id).first()
@@ -1957,7 +2004,7 @@ def get_queue_status(queue_id: int, db: Session = Depends(get_db)):
}
@app.get("/api/queue/active")
@protected_router.get("/api/queue/active")
def get_queue_info(db: Session = Depends(get_db)):
"""Get queue overview: pending and processing counts."""
pending = db.query(func.count(ReportQueue.id)).filter(ReportQueue.status == "PENDING").scalar() or 0
@@ -1967,7 +2014,7 @@ def get_queue_info(db: Session = Depends(get_db)):
# Update report
@app.get("/api/reports/{report_id}", response_model=ReportResponse)
@protected_router.get("/api/reports/{report_id}", response_model=ReportResponse)
def get_report(report_id: int, db: Session = Depends(get_db)):
"""Get a specific report by ID"""
report = db.query(Report).filter(Report.id == report_id).first()
@@ -1994,7 +2041,7 @@ def get_report(report_id: int, db: Session = Depends(get_db)):
scraping_progress = {}
# Get scraping progress
@app.get("/api/reports/progress/{task_id}")
@protected_router.get("/api/reports/progress/{task_id}")
def get_scraping_progress(task_id: str):
"""Get real-time scraping progress"""
if task_id not in scraping_progress:
@@ -2185,7 +2232,7 @@ def scrape_in_background(task_id: str, report_name: str, category_id: int, categ
# Create report with real-time SSE progress (SYNCHRONOUS)
@app.put("/api/reports/{report_id}")
@protected_router.put("/api/reports/{report_id}")
def update_report(report_id: int, report: ReportUpdate, db: Session = Depends(get_db)):
"""Update report name"""
db_report = db.query(Report).filter(Report.id == report_id).first()
@@ -2200,7 +2247,7 @@ def update_report(report_id: int, report: ReportUpdate, db: Session = Depends(ge
# Delete report
@app.delete("/api/reports/{report_id}")
@protected_router.delete("/api/reports/{report_id}")
def delete_report(report_id: int, db: Session = Depends(get_db)):
"""Delete a report"""
report = db.query(Report).filter(Report.id == report_id).first()
@@ -2235,7 +2282,7 @@ import hashlib
dashboard_cache = {}
DASHBOARD_CACHE_TTL = 3600 # 1 hour in seconds
@app.get("/api/reports/{report_id}/dashboard-data")
@protected_router.get("/api/reports/{report_id}/dashboard-data")
def get_dashboard_data(report_id: int, db: Session = Depends(get_db)):
"""
Dashboard verisi döndür — konsolide dosya varsa oku, yoksa yerinde oluştur.
@@ -2358,7 +2405,7 @@ def reviews_summary_disabled(report_id: int, refresh: bool = False, db: Session
return {"error": str(e), "note": "Network or parsing issue", "summary": {}}
@app.get("/api/reports/{report_id}/social-proof/progress")
@protected_router.get("/api/reports/{report_id}/social-proof/progress")
def social_proof_progress(report_id: int):
"""Check social proof enrichment progress"""
progress_key = f"social_{report_id}"
@@ -2367,7 +2414,7 @@ def social_proof_progress(report_id: int):
return {"status": "not_started", "progress": 0}
@app.get("/api/reports/{report_id}/social-proof")
@protected_router.get("/api/reports/{report_id}/social-proof")
def social_proof(report_id: int, refresh: bool = False, batch_size: int = 5, db: Session = Depends(get_db)):
# Try persistent cache first
if not refresh:
@@ -2529,7 +2576,7 @@ def social_proof(report_id: int, refresh: bool = False, batch_size: int = 5, db:
return {"error": str(e), "note": "Network or parsing issue", "aggregation": {}, "details": {}}
@app.get("/api/reports/{report_id}/sales-analytics")
@protected_router.get("/api/reports/{report_id}/sales-analytics")
def sales_analytics(report_id: int):
"""
Fast sales analytics endpoint - returns top products by orders
@@ -2707,7 +2754,7 @@ def generate_ngrams(tokens: list, min_n: int = 1, max_n: int = 3) -> list:
return ngrams
@app.get("/api/reports/{report_id}/keyword-analysis")
@protected_router.get("/api/reports/{report_id}/keyword-analysis")
def keyword_analysis(
report_id: int,
min_frequency: int = 3,
@@ -3227,7 +3274,7 @@ def keyword_analysis(
# PRODUCT FINDER
# ============================================================================
@app.get("/api/reports/{report_id}/product-finder")
@protected_router.get("/api/reports/{report_id}/product-finder")
def product_finder(
report_id: int,
keywords: Optional[str] = None,
@@ -3474,7 +3521,7 @@ def product_finder(
"conversion_rate": round(conversion_rate, 2),
"origin_country": origin_country,
"image_url": product.get("imageUrl", ""),
"barcode": product.get("barcode", "")
"barcode": product.get("barcode", ""),
})
log_api.info(f"Filtreleme sonrası: {len(filtered_products)} ürün kaldı")
@@ -3699,9 +3746,8 @@ def _enrich_build_product_info(all_products):
return info
def _enrich_report_task(report_id: int, progress_cb=None):
def _enrich_report_task(report_id: int):
import time
import threading
db = SessionLocal()
try:
enrichment_progress[report_id] = {"status": "running", "step": "init", "done": 0, "total": 2}
@@ -3719,28 +3765,9 @@ def _enrich_report_task(report_id: int, progress_cb=None):
# _save_json(f"{base_dir}/reviews.json", rev_payload)
# time.sleep(0.1)
# 2) Social Proof — monitor internal batch progress and relay to queue
# 2) Social Proof
enrichment_progress[report_id] = {"status": "running", "step": "social", "done": 0, "total": 1}
stop_monitor = threading.Event()
if progress_cb:
def _monitor_social():
progress_key = f"social_{report_id}"
last_queue_pct = 95
while not stop_monitor.wait(30):
ep = enrichment_progress.get(progress_key) or {}
done = ep.get("processed", 0)
total = ep.get("total", 0) or 1
queue_pct = 96 + min(3, int((done / total) * 4))
if queue_pct > last_queue_pct:
last_queue_pct = queue_pct
try:
progress_cb(queue_pct, f"Sosyal kanit: {done}/{total} urun")
except Exception:
pass
threading.Thread(target=_monitor_social, daemon=True).start()
soc_payload = social_proof(report_id, refresh=True, db=db) or {}
stop_monitor.set()
# ürün isimlerini detaylara iliştir
if soc_payload and soc_payload.get("details"):
details = soc_payload["details"]
@@ -3794,14 +3821,14 @@ def _enrich_report_task(report_id: int, progress_cb=None):
db.close()
@app.post("/api/reports/{report_id}/enrich/start")
@protected_router.post("/api/reports/{report_id}/enrich/start")
def start_enrichment(report_id: int, background: BackgroundTasks):
enrichment_progress[report_id] = {"status": "queued", "step": "queued"}
background.add_task(_enrich_report_task, report_id)
return {"status": "started", "report_id": report_id}
@app.get("/api/reports/{report_id}/enrich/status")
@protected_router.get("/api/reports/{report_id}/enrich/status")
def enrichment_status(report_id: int):
result = enrichment_progress.get(report_id)
return result if result is not None else {"status": "unknown"}
@@ -3811,7 +3838,7 @@ def enrichment_status(report_id: int):
# HIDDEN CHAMPIONS ENDPOINT
# ============================================================================
@app.get("/api/reports/{report_id}/hidden-champions")
@protected_router.get("/api/reports/{report_id}/hidden-champions")
def get_hidden_champions(
report_id: int,
min_rating: float = 4.5,
@@ -3864,7 +3891,7 @@ def get_hidden_champions(
# ANALYTICS TEST ENDPOINT
# ============================================================================
@app.get("/api/reports/{report_id}/test-analytics")
@protected_router.get("/api/reports/{report_id}/test-analytics")
def test_analytics(report_id: int, db: Session = Depends(get_db)):
"""
Test endpoint: HHI Index ve Risk Skoru hesaplama testi
@@ -4001,119 +4028,6 @@ def test_analytics(report_id: int, db: Session = Depends(get_db)):
}
# ============================================================================
# GOOGLE TRENDS & TRAFFIC SOURCES ENDPOINTS
# ============================================================================
@app.get("/api/products/traffic-sources")
async def get_traffic_sources_bulk(product_names: str):
"""
Get estimated traffic sources for multiple products
Args:
product_names: Comma-separated product names
Returns:
Dict with traffic source estimates for each product
"""
try:
# Split product names
names_list = [name.strip() for name in product_names.split(',')]
results = {}
for product_name in names_list[:10]: # Limit to 10 products at once
if not product_name:
continue
# Use default social proof values (will be replaced with real data in frontend)
traffic_data = estimate_traffic_sources(
product_name=product_name,
instagram_views=0,
tiktok_views=0,
twitter_shares=0
)
results[product_name] = traffic_data
return {
'success': True,
'results': results,
'total': len(results)
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/products/traffic-sources/estimate")
async def estimate_product_traffic_sources(request_data: dict):
"""
Estimate traffic sources for a single product with social proof data
Request Body:
{
"product_name": "Casio Edifice Kol Saati",
"instagram_views": 10000,
"tiktok_views": 5000,
"twitter_shares": 500
}
Returns:
Traffic source percentage distribution
"""
try:
product_name = request_data.get('product_name', '')
instagram_views = request_data.get('instagram_views', 0)
tiktok_views = request_data.get('tiktok_views', 0)
twitter_shares = request_data.get('twitter_shares', 0)
if not product_name:
raise HTTPException(status_code=400, detail="product_name is required")
# Estimate traffic sources
traffic_data = estimate_traffic_sources(
product_name=product_name,
instagram_views=instagram_views,
tiktok_views=tiktok_views,
twitter_shares=twitter_shares
)
return {
'success': True,
'product_name': product_name,
'traffic_sources': traffic_data
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/google-trends/test")
async def test_google_trends(product_name: str = "iPhone 15"):
"""
Test endpoint for Google Trends API
Args:
product_name: Product name to search (default: iPhone 15)
Returns:
Google Trends data
"""
try:
trends_data = fetch_google_trends(product_name)
return {
'success': True,
'product_name': product_name,
'trends_data': trends_data
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ---------------------------------------------------------------------------
# Periodic resource logger (runs every 60s in background)
# ---------------------------------------------------------------------------
@@ -4136,6 +4050,9 @@ async def _periodic_resource_log():
except Exception:
pass # Never crash the background task
# ── Router Registration ──────────────────────────────────────────────────────
app.include_router(protected_router)
@app.on_event("startup")
async def _start_resource_logger():
asyncio.create_task(_periodic_resource_log())
@@ -4160,39 +4077,6 @@ async def _stop_queue_worker():
_queue_worker = None
# ---------------------------------------------------------------------------
# Public product lookup endpoint (Ne Kadar Sattı? feature)
# ---------------------------------------------------------------------------
@app.get("/api/product/lookup", dependencies=[Depends(verify_api_key)])
def product_lookup(url: str, request: Request):
ip = request.headers.get("X-Forwarded-For", request.client.host or "unknown").split(",")[0].strip()
if not _check_ip_rate_limit(ip):
raise HTTPException(status_code=429, detail="Çok fazla istek. Bir dakika sonra tekrar deneyin.")
if not url or "trendyol.com" not in url.lower():
raise HTTPException(status_code=400, detail="Geçerli bir Trendyol URL'si girin.")
m = _TRENDYOL_URL_RE.search(url)
if not m:
raise HTTPException(status_code=400, detail="URL'den ürün ID'si alınamadı.")
product_id = int(m.group(1))
cached = product_lookup_cache.get(str(product_id))
if cached:
return {"source": "cache", "productId": product_id, **cached}
if _social_proof_breaker.is_open():
raise HTTPException(status_code=503, detail="Servis geçici olarak kullanılamıyor.")
data = fetch_social_proof([product_id])
if not data:
_social_proof_breaker.record_failure()
raise HTTPException(status_code=404, detail="Bu ürün için veri bulunamadı.")
items = data.get("result") or []
pd_item = next((it for it in items if it.get("contentId") == product_id), None)
if not pd_item:
raise HTTPException(status_code=404, detail="Sosyal kanıt verisi bulunamadı.")
result = {k: pd_item.get(k, 0) for k in ["orderCount", "favoriteCount", "pageViewCount", "basketCount"]}
product_lookup_cache.set(str(product_id), result)
_social_proof_breaker.record_success()
return {"source": "live", "productId": product_id, **result}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)