Jakub Zych 92e1b4e09c 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
2026-10-04 00:10:15 +02:00

golem15.feedback

The shared feedback-widget plugin for SummerCMS applications: an embeddable widget's public config and submit API behind a widget key and an Origin allow-list, a per-user "hide the widget" preference, the widget settings, and a job that files every submission as a G15Office task.

import _ "git.golem15.com/golem15/sm-feedback-plugin"

Module git.golem15.com/golem15/sm-feedback-plugin, package feedback, plugin ID golem15.feedback.

Overview

The plugin is a compiled SummerCMS plugin ported from the WinterCMS Golem15.Feedback plugin. Its init registers it with the framework, so an application loads it by listing it in summer.yaml; summer build then emits the blank import above into the application's plugins.gen.go. It requires golem15.user.

The widget routes are public. The widget key in the URL is a public identifier (it ships in the embed snippet), not a secret: the real gate is the Origin check against the operator's allowed-origins list, backed by per-IP rate limits.

The plugin ID, the table names, the config keys and the settings code are a contract shared by every application that mounts the plugin: change them only in a backwards-compatible way.

Features

  • Widget config. GET /_feedback/api/v1/{key}/config answers the widget's position, whether visitors may hide it, its colors and its labels (?lang=en selects the English labels, anything else the Polish ones). A disabled widget, an empty key or a key that differs (compared in constant time) is 404 {"error":true,"message":"Not found"}.
  • Origin gate. A request without an Origin header passes. An Origin passes only when its URL host, lower-cased, is on the allowed-origins list (one entry per line; a line is reduced to its URL host when it has one). An empty list refuses every Origin with 403 {"error":true,"message":"Origin not allowed"}, so a widget that was never configured cannot be embedded anywhere.
  • Submissions. POST /_feedback/api/v1/{key}/submit (multipart, from the widget) passes the same key and Origin gate, then validates the report as the WinterCMS plugin does: message (required, up to 5000 characters), type (bug, feature or other), email (optional, up to 255), page_url (required, up to 2000), user_agent (up to 500), console_log (up to 20000) and screenshot (an optional JPEG, PNG, GIF or WebP of up to 10240 KB). A failure is 422 {"error":true,"message":"Validation failed","details":{...}}. A screenshot whose bytes do not decode as one of those images (classes.IsAllowedImage) is 422 with details.screenshot = ["The file is not a valid image."]. The submission (status pending), its public screenshot (attachOne screenshot) and its G15Office job are written in one transaction, and the answer is 202 {"success":true}: the visitor never waits on G15Office.
  • Hiding the widget. PUT /_feedback/api/v1/me/hidden (behind golem15.user's jwt.auth) stores the signed-in user's preference: hidden must be a Laravel boolean (true, false, 1, 0, "1", "0"), else 422 {"error":"Validation failed","errors":{...}}. The answer is {"hidden":bool}. Every golem15.user payload (fetch, login, update) carries feedback_widget_hidden through a GetApiArrayEvent listener: the stored value, false without a row.
  • CORS. The widget's preflights are answered by the framework's path-scoped CORS layer, so the plugin registers no OPTIONS route.
  • Rate limits. The named buckets feedback-config (60 a minute) and feedback-submit (10 a minute) count per client IP behind the configured trusted proxies.
  • Settings. models.Settings is the singleton row of golem15_feedback_settings. models.Instance creates it on first access with the WinterCMS defaults and a new widget key (wk_ plus 32 random letters and digits).

Usage

Mount the plugin in an application as a git submodule at plugins/golem15/feedback:

git submodule add git@git.golem15.com:golem15/sm-feedback-plugin.git plugins/golem15/feedback

Add it to the application's go.work and go.mod:

use ./plugins/golem15/feedback

require git.golem15.com/golem15/sm-feedback-plugin v0.0.0

replace git.golem15.com/golem15/sm-feedback-plugin => ./plugins/golem15/feedback

List it in summer.yaml after golem15.user and run summer build:

module: git.golem15.com/golem15/acme
binary: acme
plugins:
  - id: golem15.user
    module: git.golem15.com/golem15/sm-user-plugin
  - id: golem15.feedback
    module: git.golem15.com/golem15/sm-feedback-plugin

The application's http.cors.paths must list _feedback/api/*: the widget calls the API from other origins.

The plugin's own go.mod replaces the framework with ../../../../summercms.go and the user plugin with ../user, so it expects the framework checkout next to the application checkout and the user plugin mounted beside it.

API reference

  • feedback: Plugin, the golem15.feedback plugin (config, lang, migrations, models, routes, buckets); BucketConfig, BucketSubmit.
  • controllers/api: Config, Submit, SubmitRules, MeHidden, MeHiddenRules, KeyMatches, OriginAllowed.
  • classes: IsAllowedImage, SniffImageMIME, AllowedImageMIMEs; Screenshot, StoreSubmission, ScreenshotOf; the job contract SyncG15OfficeArgs, SyncG15OfficeKind, SyncG15OfficeQueue, SyncG15OfficeLabel, SyncG15OfficeAttempts.
  • models: Settings (table golem15_feedback_settings), SettingsID, SettingsCode, DefaultSettings, Instance, NewWidgetKey, Labels, URLHost, ASCIILower; Submission (table golem15_feedback_submissions), StatusPending, StatusSent, StatusFailed, TypeBug, TypeFeature, TypeOther, ScreenshotField, G15OfficeTypeMap; UserPreference (table golem15_feedback_user_preferences), IsWidgetHidden, SetWidgetHidden; Register, All.
  • updates: the migrations (202610030201_create_feedback_submissions, 202610030202_create_feedback_user_preferences, 202610030203_create_feedback_settings), Register, All.

Configuration

Defaults live in config/config.yaml and are merged under golem15.feedback.*.

Key Default Meaning
golem15.feedback.g15_office.base_url empty The G15Office base URL submissions are filed under.
golem15.feedback.g15_office.token empty The G15Office API token. Never commit it.
golem15.feedback.g15_office.project empty The G15Office project the tasks are created in.

Override them with SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__BASE_URL, SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__TOKEN and SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__PROJECT.

Dependencies

  • The SummerCMS framework, git.golem15.com/golem15/summercms (modules backpack, bouncer, compass, conga, lagoon, pact, party, phrasebook, surf, wire).
  • gocloud.dev blob storage for the screenshots, through the application's uploads bucket.
  • The user plugin, git.golem15.com/golem15/sm-user-plugin.
  • GORM and gormigrate, as pinned in go.mod.
  • testcontainers-go for the Postgres-backed tests.

Testing

go test ./...   # starts Postgres with testcontainers (needs Docker)
Description
No description provided
Readme 188 KiB
Languages
Go 83.7%
JavaScript 16.3%