feat(sales): redirect beta-signup to /register; queue waitlist emails
Phase 2 retires the public beta-signup form in favor of the self-serve register flow. The /api/v1/beta-signup POST endpoint stays mounted but now responds with 307 to /register?from=beta so any external links keep working and analytics can tag signup origin via the from query param. Note: there is no beta_signup table in the schema — the original endpoint only fired an email notification, so there is no waitlist to read and no migration to run for the email-sent_at field. The one-off admin script in the spec is therefore a no-op and is intentionally not added here. - Replace POST /beta-signup handler with RedirectResponse(307) - Drop the EmailService.send_beta_signup_notification call (the user is now redirected into the register flow, which has its own email path) - Add tests/test_beta_signup_redirect.py covering the 307 + Location Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +1,44 @@
|
||||
"""Public beta signup endpoint — no auth required."""
|
||||
"""Legacy beta signup endpoint — redirects to /register?from=beta.
|
||||
|
||||
Phase 2 (self-serve signup) makes the public register flow the canonical
|
||||
front door. The old `/api/v1/beta-signup` POST endpoint is kept mounted to
|
||||
preserve any external links that still hit it, but now responds with a
|
||||
307 Temporary Redirect to `/register?from=beta` so the user lands in the
|
||||
real signup flow. The `?from=beta` marker lets the frontend tag the
|
||||
signup origin for analytics.
|
||||
|
||||
Note: there is no `beta_signup` database table — the original endpoint
|
||||
only fired a notification email. There is therefore no waitlist to email
|
||||
and no migration to run when retiring the endpoint.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from app.core.email import EmailService
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/beta-signup", tags=["beta"])
|
||||
|
||||
|
||||
class BetaSignupRequest(BaseModel):
|
||||
email: EmailStr
|
||||
# Local-dev fallback when FRONTEND_URL isn't configured. The redirect must
|
||||
# be absolute — a relative URL would resolve against the API origin
|
||||
# (api.resolutionflow.com), which has no /register page.
|
||||
_DEFAULT_FRONTEND_URL = "http://localhost:5173"
|
||||
|
||||
|
||||
class BetaSignupResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
@router.post("", include_in_schema=False)
|
||||
async def beta_signup_redirect() -> RedirectResponse:
|
||||
"""Redirect legacy beta-signup POST to the public register page.
|
||||
|
||||
|
||||
@router.post("", response_model=BetaSignupResponse)
|
||||
async def beta_signup(data: BetaSignupRequest):
|
||||
"""Collect beta interest — sends notification to beta@resolutionflow.com."""
|
||||
sent = await EmailService.send_beta_signup_notification(data.email)
|
||||
if not sent:
|
||||
logger.warning("Beta signup recorded (email delivery skipped): %s", data.email)
|
||||
return BetaSignupResponse(
|
||||
success=True,
|
||||
message="Thanks! We'll be in touch with beta access details.",
|
||||
Returns 307 so any client following the redirect preserves the HTTP
|
||||
method; the frontend treats `/register?from=beta` as the canonical
|
||||
entry point and reads the `from` query param for analytics.
|
||||
"""
|
||||
frontend_url = settings.FRONTEND_URL or _DEFAULT_FRONTEND_URL
|
||||
return RedirectResponse(
|
||||
url=f"{frontend_url}/register?from=beta",
|
||||
status_code=307,
|
||||
)
|
||||
|
||||
43
backend/tests/test_beta_signup_redirect.py
Normal file
43
backend/tests/test_beta_signup_redirect.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Integration tests for the legacy /beta-signup redirect.
|
||||
|
||||
Phase 2 retires the public beta-signup form in favor of the regular
|
||||
register flow. The endpoint stays mounted but answers with a 307 to
|
||||
the absolute frontend `/register?from=beta` URL so any external links
|
||||
keep working. There is no `beta_signup` table to migrate — the old
|
||||
endpoint only fired an email notification — so this test only covers
|
||||
the redirect contract.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_beta_signup_redirects_to_register(client, monkeypatch):
|
||||
"""POST /beta-signup returns 307 to the absolute frontend register URL."""
|
||||
monkeypatch.setattr(settings, "FRONTEND_URL", "https://example.com")
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/beta-signup",
|
||||
json={"email": "anyone@example.com"},
|
||||
)
|
||||
|
||||
assert response.status_code == 307, response.text
|
||||
assert (
|
||||
response.headers["location"]
|
||||
== "https://example.com/register?from=beta"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_beta_signup_redirect_ignores_body(client, monkeypatch):
|
||||
"""Redirect fires regardless of payload — no validation on the legacy route."""
|
||||
monkeypatch.setattr(settings, "FRONTEND_URL", "https://example.com")
|
||||
|
||||
response = await client.post("/api/v1/beta-signup", json={})
|
||||
assert response.status_code == 307
|
||||
assert (
|
||||
response.headers["location"]
|
||||
== "https://example.com/register?from=beta"
|
||||
)
|
||||
Reference in New Issue
Block a user