2026-10-07 11:18:06 +02:00
2026-10-07 11:18:06 +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). Operators edit it under Settings › Feedback Widget (code feedback, permission golem15.feedback.manage_settings): the WinterCMS form with cabana's field types, the colors as text fields and the widget key read-only. A row saved without a key gets a new one.
  • G15Office tasks. Every submission queues a classes.SyncG15OfficeArgs job (kind golem15.feedback.sync_g15office, queue feedback, 3 attempts, a summer_jobs row labelled with the WinterCMS job class). classes.SyncG15Office files it with classes.G15OfficeClient: POST <base>/_support/api/v1/projects/<project>/tasks with the title (Feedback: and the message collapsed and cut to 80 columns), a Markdown description (the message, the page, the reporter email and user agent, the console log cut to 4000 columns), the task type (bug → Bug, feature → Feature, other → Task), the configured task status and priority; then POST <base>/_support/api/v1/tasks/<id>/attachments with the screenshot as the multipart field file. Requests carry Authorization: Bearer <token> and Accept: application/json and go through a fetchguard.Client limited to the base URL's host (https only). Success marks the submission sent with the task id; a failure marks it failed with the error and the job is retried. The task id is stored as soon as the task is created, so a retry re-attempts only the attachment, and a submission already sent with a task id is never filed again. Without a base URL, token and project nothing is sent: the job fails with the WinterCMS "not configured" message.
  • Submissions list. Admin › Feedback › Submissions (controller golem15.feedback.submissions) is a read-only list: type, status, reporter email, page, message, task id and date, all as plain text.
  • Widget script. GET /plugins/golem15/feedback/assets/js/embed.js serves the WinterCMS plugin's embed.js byte for byte (application/javascript, with an ETag), at the path embedding pages already use.
  • Settings import. feedback:import-settings copies a WinterCMS installation's widget settings, the widget key included, into the singleton once.

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. A page embeds the widget with its key:

<script src="https://backend.example.com/plugins/golem15/feedback/assets/js/embed.js" data-key="wk_..." data-lang="en" defer></script>

Set the G15Office connection with the three SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__* variables (see Configuration) and run a queue worker: serve runs one in-process when queue.work_in_serve is on, queue:work in its own process. After moving an installation from WinterCMS, run feedback:import-settings once so embed snippets keep their widget key.

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, jobs, commands, admin controllers, permissions, navigation, settings); BucketConfig, BucketSubmit, EmbedJSPath, EmbedJS, SettingsCode.
  • controllers/api: Config, Submit, SubmitRules, MeHidden, MeHiddenRules, KeyMatches, OriginAllowed.
  • classes: IsAllowedImage, SniffImageMIME, AllowedImageMIMEs; Screenshot, StoreSubmission, ScreenshotOf; the job contract SyncG15OfficeArgs, SyncG15OfficeKind, SyncG15OfficeQueue, SyncG15OfficeLabel, SyncG15OfficeAttempts; the job SyncG15Office, SyncDeps, TaskTitle, TaskDescription, StrLimit, StrWidth; the client G15OfficeClient, NewG15OfficeClient, NewG15OfficeClientWith, TaskFields, ErrNotConfigured, G15OfficeTimeout, ConfigBaseURL, ConfigToken, ConfigProject.
  • controllers: SubmissionsAdminController, AdminControllers, PermissionManageSettings.
  • console: ImportSettings, Import, IsNothingToImport, ErrSettingsCustomised.
  • 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 (https; requests may reach only its host).
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.

CLI commands

Command Purpose
feedback:import-settings [--force] Copy the WinterCMS system_settings row golem15_feedback_settings (widget key, origins, colors, labels, G15Office status and priority) into golem15_feedback_settings. It refuses settings that were already changed unless --force is given, and reports a missing system_settings table or row without failing.

Dependencies

  • The SummerCMS framework, git.golem15.com/golem15/summercms (modules backpack, bonfire, bouncer, cabana, compass, conga, fetchguard, lagoon, pact, party, phrasebook, surf, wire; tide in tests).
  • 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)

classes/testdata/g15office-task.upstream.yaml is the G15Office exchange the WinterCMS job made, recorded through the framework's upstream recording proxy; TestSyncG15Office replays it offline and fails on any difference in the requests (URL, headers, the task JSON, the screenshot part).

Description
No description provided
Readme 188 KiB
Languages
Go 83.7%
JavaScript 16.3%