feat: feedback submissions with screenshots, the hide-widget preference and its user payload key

- POST /_feedback/api/v1/{key}/submit validates as PHP, refuses non-image screenshots (ImageContentGuard copy) and stores the submission, its public screenshot and the G15Office job in one transaction
- PUT /_feedback/api/v1/me/hidden (jwt.auth) stores the caller's preference; feedback_widget_hidden joins every golem15.user payload
- preflights stay with the framework CORS layer
This commit is contained in:
Jakub Zych
2026-10-04 00:10:15 +02:00
parent b4b4135ad3
commit 92e1b4e09c
12 changed files with 1073 additions and 6 deletions

View File

@@ -0,0 +1,66 @@
package api
import (
"net/http"
"git.golem15.com/golem15/sm-feedback-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/phrasebook"
)
// MeHiddenRules are MeHiddenController::update's validation rules.
var MeHiddenRules = []lagoon.RequestRule{
{Field: "hidden", Rules: lagoon.ParseRules("required|boolean")},
}
type meHiddenErrors struct {
Error string `json:"error"`
Errors any `json:"errors"`
}
type meHiddenBody struct {
Hidden bool `json:"hidden"`
}
// MeHidden handles PUT /_feedback/api/v1/me/hidden (jwt.auth): the signed-in
// user's "hide the widget" preference. hidden must be a Laravel boolean
// (true, false, 1, 0, "1", "0"); anything else is 422 {"error":"Validation
// failed","errors":...}. Only the JWT principal's row is written, and the
// answer is {"hidden":bool}. The value also rides on every user payload
// (feedback_widget_hidden), so there is no read route.
func MeHidden(app *backpack.App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
gdb := database(app)
user, ok := bouncer.User(r.Context())
if gdb == nil || !ok || user == nil || user.ID == 0 {
writeOpaque500(w)
return
}
input, ok := readInput(w, r)
if !ok {
return
}
ctx := r.Context()
tr, _ := app.Lookup[*phrasebook.Translator]()
errs, err := lagoon.ValidateRequest(ctx, gdb.WithContext(ctx), input, MeHiddenRules, tr)
if err != nil {
writeOpaque500(w)
return
}
if len(errs) > 0 {
writeJSON(w, http.StatusUnprocessableEntity, meHiddenErrors{
Error: "Validation failed",
Errors: orderedErrors{errs: errs, order: lagoon.ErrorKeys(errs, MeHiddenRules)},
})
return
}
hidden := laravelTrue(input["hidden"])
if err := models.SetWidgetHidden(ctx, gdb, user.ID, hidden); err != nil {
writeOpaque500(w)
return
}
writeJSON(w, http.StatusOK, meHiddenBody{Hidden: hidden})
}
}