mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-05 00:01:49 +00:00
119 lines
3.9 KiB
Go
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})
|
|
}
|