49 lines
1.4 KiB
Python
49 lines
1.4 KiB
Python
import logging
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.db import get_db
|
|
from app.rate_limit import _get_client_ip
|
|
from app.schemas import WaitlistRequest, WaitlistResponse
|
|
from app.models import WaitlistEntry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/waitlist", response_model=WaitlistResponse)
|
|
async def join_waitlist(
|
|
body: WaitlistRequest,
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> WaitlistResponse:
|
|
"""
|
|
Add an email to the waitlist.
|
|
|
|
- Honeypot: if `website` field is non-empty, silently succeed (bot trap).
|
|
- Duplicate emails: idempotent — returns success without error.
|
|
- Stores the Cloudflare-resolved client IP for analytics (not exposed).
|
|
"""
|
|
# Honeypot — bots fill hidden fields; silently "succeed"
|
|
if body.website:
|
|
return WaitlistResponse()
|
|
|
|
email = body.email.lower().strip()
|
|
ip = _get_client_ip(request)
|
|
|
|
# Check for existing entry — idempotent
|
|
existing = await db.execute(
|
|
select(WaitlistEntry.id).where(WaitlistEntry.email == email)
|
|
)
|
|
if existing.scalar_one_or_none() is not None:
|
|
return WaitlistResponse()
|
|
|
|
entry = WaitlistEntry(email=email, ip_address=ip, source="website")
|
|
db.add(entry)
|
|
await db.commit()
|
|
|
|
logger.info("New waitlist signup: %s", email[:3] + "***")
|
|
return WaitlistResponse()
|