Three self-contained features, verified complete and cross-wired end-to-end (audited: backend 54/54 tests, frontend 110/110 tests, tsc --noEmit clean, i18n coverage script clean): - EMF mode: DeviceMotion/DeviceOrientation-based field-meter sensing, a fifth séance channel alongside Wire/EVP/Radio/Ouija, with its own fragment prompt persona and full frontend gauge UI. - Armory (shop/waitlist): pre-order capture page for the future Ultimate Quantum Box hardware line, rate-limited public endpoint, explicitly no payment collection. - Haunting layer: ambient possession effects (dread-bed audio, title glitching, idle-paced whispers/manifests), respects prefers-reduced-motion, mounted once at the app root. Plus WebUSB robustness fixes in lib/sdr.ts (Terratec vendor ID, explicit selectConfiguration, isSecureContext gate). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
51 lines
1.6 KiB
Python
51 lines
1.6 KiB
Python
"""Shop endpoints: the waitlist for the Ultimate Quantum Box hardware."""
|
|
|
|
import re
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.db import get_db
|
|
from app.models.waitlist_entry import WaitlistEntry
|
|
from app.rate_limit import RateLimiter
|
|
|
|
router = APIRouter(prefix="/api/shop", tags=["shop"])
|
|
|
|
waitlist_limiter = RateLimiter(max_requests=5, window_seconds=3600)
|
|
|
|
_EMAIL_RE = re.compile(r"^[^@\s]{1,64}@[^@\s]{1,255}\.[^@\s]{2,}$")
|
|
|
|
|
|
@router.post("/waitlist", status_code=status.HTTP_201_CREATED)
|
|
async def join_waitlist(
|
|
payload: dict, request: Request, db: AsyncSession = Depends(get_db)
|
|
):
|
|
email = str(payload.get("email", "")).strip().lower()
|
|
interest = payload.get("interest")
|
|
if not _EMAIL_RE.match(email):
|
|
raise HTTPException(
|
|
status.HTTP_422_UNPROCESSABLE_ENTITY, "that address does not reach us"
|
|
)
|
|
|
|
client_ip = request.client.host if request.client else "unknown"
|
|
if not waitlist_limiter.allow(client_ip):
|
|
raise HTTPException(
|
|
status.HTTP_429_TOO_MANY_REQUESTS,
|
|
"the veil is crowded — try again later",
|
|
)
|
|
|
|
existing = await db.scalar(
|
|
select(WaitlistEntry).where(WaitlistEntry.email == email)
|
|
)
|
|
if existing is not None:
|
|
# Idempotent: re-registering the same address is a no-op.
|
|
return {"status": "already_listed", "email": email}
|
|
|
|
entry = WaitlistEntry(
|
|
email=email, interest=str(interest)[:64] if interest else None
|
|
)
|
|
db.add(entry)
|
|
await db.commit()
|
|
return {"status": "listed", "email": email}
|