feat(affiliate): /go/<slug> click redirect with rate limiting + click logging
302 redirect (not 301) so every click is tracked. Extracts lang/article_slug from Referer header best-effort. Rate-limited to 60/min per IP; clicks above limit still redirect but are not logged to prevent amplification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -280,6 +280,49 @@ def create_app() -> Quart:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"status": "unhealthy", "db": str(e)}, 500
|
return {"status": "unhealthy", "db": str(e)}, 500
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Affiliate click redirect — language-agnostic, no blueprint prefix
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@app.route("/go/<slug>")
|
||||||
|
async def affiliate_redirect(slug: str):
|
||||||
|
"""302 redirect to affiliate URL, logging the click.
|
||||||
|
|
||||||
|
Uses 302 (not 301) so every hit is tracked — browsers don't cache 302s.
|
||||||
|
Extracts article_slug and lang from Referer header best-effort.
|
||||||
|
"""
|
||||||
|
from .affiliate import get_product, log_click
|
||||||
|
from .core import check_rate_limit
|
||||||
|
|
||||||
|
# Extract lang from Referer path (e.g. /de/blog/... → "de"), default de
|
||||||
|
referer = request.headers.get("Referer", "")
|
||||||
|
lang = "de"
|
||||||
|
article_slug = None
|
||||||
|
if referer:
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
ref_path = urlparse(referer).path
|
||||||
|
parts = ref_path.strip("/").split("/")
|
||||||
|
if parts and len(parts[0]) == 2:
|
||||||
|
lang = parts[0]
|
||||||
|
if len(parts) > 1:
|
||||||
|
article_slug = parts[-1] or None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
product = await get_product(slug, lang)
|
||||||
|
if not product:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
ip = request.remote_addr or "unknown"
|
||||||
|
allowed, _info = await check_rate_limit(f"aff:{ip}", limit=60, window=60)
|
||||||
|
if not allowed:
|
||||||
|
# Still redirect even if rate-limited; just don't log the click
|
||||||
|
return redirect(product["affiliate_url"], 302)
|
||||||
|
|
||||||
|
await log_click(product["id"], ip, article_slug, referer or None)
|
||||||
|
return redirect(product["affiliate_url"], 302)
|
||||||
|
|
||||||
# Legacy 301 redirects — bookmarked/cached URLs before lang prefixes existed
|
# Legacy 301 redirects — bookmarked/cached URLs before lang prefixes existed
|
||||||
@app.route("/terms")
|
@app.route("/terms")
|
||||||
async def legacy_terms():
|
async def legacy_terms():
|
||||||
|
|||||||
Reference in New Issue
Block a user