pdf-quiz-generator/backend/app/routers/contact.py
Daniel 5403d78a3e Fix landing page routing, add logged-in nav integration
- App.jsx: simplify to single Routes block with Outlet layout pattern — fixes 404
- App.jsx: /home always accessible regardless of auth state
- LandingPage: show Dashboard/Quizzes/Question Bank nav links + Go to App CTA when logged in
- LandingPage: hero shows app navigation buttons when signed in
- LandingPage: fix apostrophe JSX syntax error in textarea placeholder

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

123 lines
4.5 KiB
Python

"""Contact form — stores submissions and emails admin.
Table is created via raw SQL in setup_pgvector() to avoid race conditions with multiple workers.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.database import get_db
from app.config import settings
router = APIRouter()
log = logging.getLogger(__name__)
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")
db.execute(text(
"INSERT INTO contact_submissions (name, email, type, message) VALUES (:name, :email, :type, :message)"
), {"name": name, "email": req.email.lower().strip(), "type": req.type, "message": message})
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.execute(text(
"SELECT id, name, email, type, message, read, created_at FROM contact_submissions ORDER BY created_at DESC"
)).fetchall()
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),
):
result = db.execute(text("UPDATE contact_submissions SET read=1 WHERE id=:id"), {"id": submission_id})
db.commit()
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="Not found")
return {"success": True}