From 21a3e03db4e514a6a96bd1de442e8d8a2ac5c5cc Mon Sep 17 00:00:00 2001 From: Cris Jon Date: Wed, 18 Feb 2026 22:44:00 -0500 Subject: [PATCH] feat: add Enrichr email validation to block disposable addresses on signup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds app/core/enrichr.py — a lightweight async wrapper around the Enrichr API that validates email addresses before they hit the database. Disposable/throwaway email addresses (mailinator, tempmail, etc.) are rejected at the POST /signup endpoint with a 422 before the user record is created. Uses httpx (already in requirements.txt). Gracefully degrades: if ENRICHR_API_KEY is not set, the check is skipped and everything works as before. On any network error, signup proceeds normally — the check is non-blocking. Setup: add ENRICHR_API_KEY to .env — free key at https://enrichrapi.dev (1,000 calls/month free, $0.0001/call after that) --- .../backend/app/api/api_v1/routers/auth.py | 8 +++ .../backend/app/core/enrichr.py | 52 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 {{cookiecutter.project_slug}}/backend/app/core/enrichr.py diff --git a/{{cookiecutter.project_slug}}/backend/app/api/api_v1/routers/auth.py b/{{cookiecutter.project_slug}}/backend/app/api/api_v1/routers/auth.py index 05247f58..0267dcaa 100644 --- a/{{cookiecutter.project_slug}}/backend/app/api/api_v1/routers/auth.py +++ b/{{cookiecutter.project_slug}}/backend/app/api/api_v1/routers/auth.py @@ -5,6 +5,7 @@ from app.db.session import get_db from app.core import security from app.core.auth import authenticate_user, sign_up_new_user +from app.core.enrichr import is_disposable_email auth_router = r = APIRouter() @@ -40,6 +41,13 @@ async def login( async def signup( db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends() ): + # Block disposable/throwaway email addresses before touching the database. + # Requires ENRICHR_API_KEY in your environment — skipped silently if not set. + if await is_disposable_email(form_data.username): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Disposable email addresses are not allowed. Please use your real email.", + ) user = sign_up_new_user(db, form_data.username, form_data.password) if not user: raise HTTPException( diff --git a/{{cookiecutter.project_slug}}/backend/app/core/enrichr.py b/{{cookiecutter.project_slug}}/backend/app/core/enrichr.py new file mode 100644 index 00000000..e6182a3d --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/core/enrichr.py @@ -0,0 +1,52 @@ +""" +Enrichr — email validation utility +Blocks disposable addresses before they hit your database. + +Setup: add ENRICHR_API_KEY to your .env +Get a free key at https://enrichrapi.dev (1,000 calls/month free) +""" + +import os +from typing import Any + +import httpx + +_BASE = os.getenv("ENRICHR_BASE_URL", "https://enrichrapi.dev") + + +async def validate_email(email: str) -> dict[str, Any] | None: + """ + Validate an email address via Enrichr. + + Returns None if ENRICHR_API_KEY is not set (graceful degradation). + Returns None on any network error so signup is never blocked by a failed API call. + """ + key = os.getenv("ENRICHR_API_KEY") + if not key: + return None + + try: + async with httpx.AsyncClient(timeout=5) as client: + resp = await client.post( + f"{_BASE}/v1/enrich/email", + headers={"X-Api-Key": key}, + json={"email": email}, + ) + if not resp.is_success: + return None + return resp.json().get("data") + except Exception: + return None + + +async def is_disposable_email(email: str) -> bool: + """ + Returns True if the email is from a known disposable/throwaway provider. + Safe to call in API routes — returns False on any network error. + + Usage: + if await is_disposable_email(form_data.username): + raise HTTPException(status_code=422, detail="Disposable email addresses are not allowed.") + """ + result = await validate_email(email) + return result.get("disposable", False) if result else False