Files
warmbly/internal/api/handler/internal_form.go
T

119 lines
3.9 KiB
Go

package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/warmbly/warmbly/internal/app/form"
"github.com/warmbly/warmbly/internal/config"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/formwire"
"github.com/warmbly/warmbly/internal/models"
)
// The forms service's slice of the internal API (INTERNAL_API_TOKEN, same
// pattern as the worker DEK proxy and the tracking link resolver): fetch a
// published form, record a funnel event, forward a submission. The service
// owns the public HTML and the per-visitor abuse checks; the pipeline that
// turns answers into contacts stays here.
func formCaptchaSiteKey(f *models.Form) string {
if !f.CaptchaEnabled || config.CaptchaProvider() == "none" {
return ""
}
return config.TurnstileSiteKey()
}
// InternalGetPublicForm resolves a published form for the forms service.
// 404 for unknown or unpublished ids, so the service can negative-cache. A
// valid ?t= ticket adds the contact's prefill values and echoes the token.
func (h *Handler) InternalGetPublicForm(c *gin.Context) {
f, xerr := h.FormService.PublicForm(c.Request.Context(), c.Param("publicID"))
if xerr != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not_found"})
return
}
out := formwire.PublicForm{
PublicID: f.PublicID,
Name: f.Name,
Fields: f.Fields,
Design: f.Design,
LogoURL: f.LogoURL,
CoverURL: f.CoverURL,
BackgroundURL: f.BackgroundURL,
AllowedDomains: f.AllowedDomains,
CaptchaSiteKey: formCaptchaSiteKey(f),
}
if token := c.Query("t"); token != "" {
if link, prefill := h.FormService.ResolveLink(c.Request.Context(), f, token); link != nil {
out.Prefill = prefill
out.LinkToken = token
}
}
c.JSON(http.StatusOK, out)
}
// InternalRecordFormEvent stores one funnel event; the forms service already
// deduped views, filtered prefetches and budgeted the source.
func (h *Handler) InternalRecordFormEvent(c *gin.Context) {
var req formwire.EventRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_body"})
return
}
xerr := h.FormService.RecordEvent(c.Request.Context(), c.Param("publicID"), form.EventInput{
Type: req.Type,
PageIndex: req.PageIndex,
PagesTotal: req.PagesTotal,
VisitorKey: req.VisitorKey,
SourceURL: req.SourceURL,
LinkToken: req.LinkToken,
RemoteIP: req.RemoteIP,
UserAgent: req.UserAgent,
})
if xerr != nil {
if xerr.Code == errx.NotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "not_found"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_event"})
return
}
c.Status(http.StatusNoContent)
}
// InternalSubmitForm runs the submission pipeline for answers the forms
// service collected. The 400 body's message is visitor-facing: the service
// shows it inline on the form.
func (h *Handler) InternalSubmitForm(c *gin.Context) {
var req formwire.SubmitRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, formwire.SubmitError{Error: "invalid_body", Message: "The submission could not be read."})
return
}
meta := form.SubmitMeta{
RemoteIP: req.RemoteIP,
SourceURL: req.SourceURL,
CaptchaToken: req.CaptchaToken,
HoneypotFilled: req.HoneypotFilled,
LinkToken: req.LinkToken,
VisitorKey: req.VisitorKey,
}
if req.RenderedAt > 0 {
meta.RenderedAt = time.Unix(req.RenderedAt, 0)
}
res, xerr := h.FormService.Submit(c.Request.Context(), c.Param("publicID"), req.Answers, meta)
if xerr != nil {
if xerr.Code == errx.NotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "not_found"})
return
}
// Message only: the errx prefix ("Bad Request (400):") is for logs,
// not for a visitor's inline error.
c.JSON(http.StatusBadRequest, formwire.SubmitError{Error: "form_submit_failed", Message: xerr.Message})
return
}
c.JSON(http.StatusOK, formwire.SubmitResult{Message: res.Message, RedirectURL: res.RedirectURL})
}