pdf-quiz-generator/backend/app/routers/contact.py
Daniel ee47fb07a2 Add PedsHub landing page with contact form and AI Scribe section
Frontend:
- LandingPage.jsx: hero, 6 feature cards, AI Scribe section (links peds.danvics.com), contact form
- Contact form: name/email/type toggle (question vs moderator app), Turnstile anti-bot (optional)
- App.jsx: /home route for unauthenticated users, auth pages reachable pre-login, logged-in /home → /

Backend:
- contact.py: POST /api/contact (public), stores in DB, emails admin; GET /submissions (admin)
- main.py: create contact_submissions table, register contact router
- main.py: add pedshub.com + www.pedshub.com to CORS origins
- config.py: TURNSTILE_SECRET_KEY, ADMIN_EMAIL settings

Env vars to set:
  VITE_TURNSTILE_SITE_KEY=<from cloudflare.com/turnstile>  (frontend)
  TURNSTILE_SECRET_KEY=<secret>  (backend .env)
  ADMIN_EMAIL=you@example.com  (backend .env)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 01:34:44 +02:00

140 lines
4.8 KiB
Python

"""Contact form — stores submissions and emails admin."""
from datetime import datetime
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.orm import Session
from app.database import get_db, Base
from app.config import settings
router = APIRouter()
log = logging.getLogger(__name__)
class ContactSubmission(Base):
__tablename__ = "contact_submissions"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
email = Column(String, nullable=False)
type = Column(String, nullable=False) # "question" | "moderator"
message = Column(Text, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
read = Column(Integer, default=0)
class ContactRequest(BaseModel):
name: str
email: EmailStr
type: str # "question" | "moderator"
message: str
turnstile_token: str | None = None
async def _verify_turnstile(token: str) -> bool:
"""Verify Cloudflare Turnstile token. Returns True if valid or if Turnstile not configured."""
secret = getattr(settings, "TURNSTILE_SECRET_KEY", None)
if not secret:
return True # Not configured — allow in dev/no-captcha mode
try:
import httpx
resp = httpx.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={"secret": secret, "response": token},
timeout=10,
)
return resp.json().get("success", False)
except Exception as e:
log.warning(f"Turnstile verification failed: {e}")
return False
@router.post("")
async def submit_contact(req: ContactRequest, db: Session = Depends(get_db)):
"""Public endpoint — no auth required."""
if req.type not in ("question", "moderator"):
raise HTTPException(status_code=400, detail="Type must be 'question' or 'moderator'")
name = req.name.strip()[:120]
message = req.message.strip()[:2000]
if not name or not message:
raise HTTPException(status_code=400, detail="Name and message are required")
# Verify Turnstile if configured
secret = getattr(settings, "TURNSTILE_SECRET_KEY", None)
if secret:
if not req.turnstile_token:
raise HTTPException(status_code=400, detail="Bot verification required")
if not await _verify_turnstile(req.turnstile_token):
raise HTTPException(status_code=400, detail="Bot verification failed — please try again")
submission = ContactSubmission(
name=name,
email=req.email.lower().strip(),
type=req.type,
message=message,
)
db.add(submission)
db.commit()
# Email admin
_email_admin(name, req.email, req.type, message)
return {"success": True}
def _email_admin(name: str, email: str, type_: str, message: str):
try:
from app.services.email_service import send_email
label = "Moderator Application" if type_ == "moderator" else "Contact Form Question"
body = (
f"<h2>New {label} — PedsHub</h2>"
f"<p><strong>From:</strong> {name} &lt;{email}&gt;</p>"
f"<p><strong>Message:</strong></p>"
f"<blockquote style='border-left:4px solid #2563eb;padding-left:12px;color:#334155'>"
f"{message.replace(chr(10), '<br>')}"
f"</blockquote>"
)
admin_email = getattr(settings, "ADMIN_EMAIL", None) or getattr(settings, "SMTP_FROM", None)
if admin_email:
send_email(to=admin_email, subject=f"[PedsHub] {label} from {name}", html=body)
except Exception as e:
log.warning(f"Failed to email admin about contact submission: {e}")
# ── Admin view of submissions ─────────────────────────────────────────────────
@router.get("/submissions")
def list_submissions(
db: Session = Depends(get_db),
):
"""Admin: list all contact submissions."""
from app.utils.auth import require_admin
rows = db.query(ContactSubmission).order_by(ContactSubmission.created_at.desc()).all()
return [
{
"id": r.id,
"name": r.name,
"email": r.email,
"type": r.type,
"message": r.message,
"read": r.read,
"created_at": r.created_at.isoformat(),
}
for r in rows
]
@router.put("/submissions/{submission_id}/read")
def mark_read(
submission_id: int,
db: Session = Depends(get_db),
):
row = db.query(ContactSubmission).filter(ContactSubmission.id == submission_id).first()
if not row:
raise HTTPException(status_code=404, detail="Not found")
row.read = 1
db.commit()
return {"success": True}