diff --git a/backend/app/routers/contact.py b/backend/app/routers/contact.py index c3ad3ff..85a4069 100644 --- a/backend/app/routers/contact.py +++ b/backend/app/routers/contact.py @@ -1,30 +1,20 @@ -"""Contact form — stores submissions and emails admin.""" -from datetime import datetime +"""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 Column, Integer, String, Text, DateTime +from sqlalchemy import text from sqlalchemy.orm import Session -from app.database import get_db, Base +from app.database import get_db 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 @@ -70,13 +60,9 @@ async def submit_contact(req: ContactRequest, db: Session = Depends(get_db)): 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.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 @@ -112,15 +98,13 @@ def list_submissions( ): """Admin: list all contact submissions.""" from app.utils.auth import require_admin - rows = db.query(ContactSubmission).order_by(ContactSubmission.created_at.desc()).all() + 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, + "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 @@ -132,9 +116,8 @@ 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 + 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} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 20ca3e3..66184b3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom' import { AuthProvider, useAuth } from './context/AuthContext' import { ThemeProvider } from './context/ThemeContext' import Navbar from './components/Navbar' @@ -23,68 +23,72 @@ import ResetPasswordPage from './pages/ResetPasswordPage' import NotFoundPage from './pages/NotFoundPage' import LandingPage from './pages/LandingPage' -function ProtectedRoute({ children, requireModerator = false }) { - const { user, loading } = useAuth() - if (loading) return