""" Public waitlist submission — no self-serve org creation is promised here, just "we'll get back to you". Not coupled to any plan/tier: the commercial model (participation-based access, not per-seat SaaS — see app/internal/tenancy.py) is still being defined separately, so this route only ever writes {email, org_name, note} and never a plan_id. Unauthenticated by design (a prospect has no account yet), so the only protection against abuse is source-IP rate limiting via the shared _RateLimiter (app/internal/auth.py). """ from datetime import datetime, timezone from typing import Optional from uuid import uuid4 from fastapi import APIRouter, Request from pydantic import BaseModel, field_validator from app.internal import firestore as fstore from app.internal.auth import waitlist_limiter from app.internal.logger import logger router = APIRouter(tags=["waitlist"]) class WaitlistBody(BaseModel): # Plain str, not pydantic.EmailStr — EmailStr needs the email-validator # package, which isn't in requirements.txt, and adding a dependency for # one light check wasn't worth it. Good-enough sanity check only; this # is a marketing capture form, not an auth path. email: str org_name: Optional[str] = None note: Optional[str] = None @field_validator("email") @classmethod def _basic_email_shape(cls, v: str) -> str: v = v.strip() if "@" not in v or " " in v or len(v) > 254: raise ValueError("Enter a valid email address.") return v @router.post("/waitlist", status_code=201) async def join_waitlist(body: WaitlistBody, request: Request): client_ip = request.client.host if request.client else "unknown" waitlist_limiter.check(client_ip) entry_id = str(uuid4()) await fstore.doc_set("waitlist", entry_id, { "entry_id": entry_id, "email": body.email.lower(), "org_name": (body.org_name or "").strip() or None, "note": (body.note or "").strip()[:2000] or None, "created_at": datetime.now(timezone.utc).isoformat(), "source_ip": client_ip, }, merge=False) logger.info(f"Waitlist signup: {body.email!r} (org_name={body.org_name!r})") return {"ok": True}