Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142package api
import ( "net/http" "strings" "unicode"
goaway "github.com/TwiN/go-away" "github.com/isabelroses/quoteit/internal/store" "github.com/labstack/echo/v4")
// isMostlyNumeric reports whether more than half of the quote's non-whitespace// characters are digits. Used to drop snowflake-ID / numeric-spam submissions.func isMostlyNumeric(s string) bool { var digits, nonSpace int for _, r := range s { if unicode.IsSpace(r) { continue } nonSpace++ if unicode.IsDigit(r) { digits++ } } return nonSpace > 0 && digits*2 > nonSpace}
// anyProfane checks each field independently — avoids the concat allocation// the old code did and short-circuits on the first hit.func anyProfane(fields ...string) bool { for _, f := range fields { if f != "" && goaway.IsProfane(f) { return true } } return false}
type addRequest struct { Quote string `json:"quote"` Author string `json:"author"` Reason string `json:"reason"`}
func (s *Server) add(c echo.Context) error { var req addRequest if err := c.Bind(&req); err != nil { return httpError(http.StatusBadRequest, "invalid json: "+err.Error()) }
req.Quote = strings.TrimSpace(req.Quote) req.Author = strings.TrimSpace(req.Author) req.Reason = strings.TrimSpace(req.Reason)
if req.Quote == "" { return httpError(http.StatusBadRequest, "quote is required") } if isMostlyNumeric(req.Quote) { return httpError(http.StatusBadRequest, "quote must be mostly text, not numbers") }
existing, dup, err := s.store.FindSimilar(c.Request().Context(), req.Quote) if err != nil { return s.fail(c, "similarity check", err, "") } if dup { body := map[string]any{"error": "a similar quote already exists"} // Only expose the matched quote if it's already public; never leak // pending or rejected content to a non-admin submitter. if existing.Status == store.StatusApproved { body["existing"] = existing } return c.JSON(http.StatusConflict, body) }
status := store.StatusApproved if anyProfane(req.Quote, req.Author, req.Reason) { status = store.StatusPending }
q := store.Quote{ Quote: req.Quote, Author: req.Author, Reason: req.Reason, Status: status, } if err := s.store.Create(c.Request().Context(), &q); err != nil { return s.fail(c, "insert", err, "") } return c.JSON(http.StatusCreated, q)}
func (s *Server) get(c echo.Context) error { q, err := s.store.Random(c.Request().Context()) if err != nil { return s.fail(c, "get", err, "no quotes yet") } return c.JSON(http.StatusOK, q)}
func (s *Server) getByID(c echo.Context) error { id, err := paramID(c) if err != nil { return err } q, err := s.store.ByID(c.Request().Context(), id) if err != nil { return s.fail(c, "get by id", err, "quote not found") } return c.JSON(http.StatusOK, q)}
func (s *Server) authors(c echo.Context) error { authors, err := s.store.Authors(c.Request().Context()) if err != nil { return s.fail(c, "authors", err, "") } return c.JSON(http.StatusOK, map[string]any{"authors": authors})}
func (s *Server) author(c echo.Context) error { name := c.Param("name") quotes, err := s.store.ByAuthor(c.Request().Context(), name) if err != nil { return s.fail(c, "author", err, "") } return c.JSON(http.StatusOK, map[string]any{ "author": name, "quotes": quotes, })}
func (s *Server) authorRandom(c echo.Context) error { name := c.Param("name") q, err := s.store.RandomByAuthor(c.Request().Context(), name) if err != nil { return s.fail(c, "author random", err, "no quotes for that author") } return c.JSON(http.StatusOK, q)}