feat(feedback): in-app user feedback with admin review and account roles
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
User-facing Feedback screen (Settings -> Info, registered accounts only): a message (<=1024 runes) plus one optional attachment, an anti-spam gate (one unreviewed message at a time), and the operator's inline reply with a Settings/Info badge. Server-rendered admin console section (/_gm/feedback): unread/read/archived queue with per-user search, detail with read/reply/ archive/delete/delete-all, safe attachment serving (nosniff, images inline via <img>, others download-only). Introduces account_roles, the first per-account role table; feedback_banned blocks only feedback submission, granted/revoked from /users and the delete-with-block action. - migration 00004_feedback (feedback_messages + account_roles) + jetgen - backend internal/feedback (store+service), internal/account/roles.go - wire: FlatBuffers feedback.submit/get/unread; gateway guest gate (Op.NonGuest, is_guest via session resolve) -> guest_forbidden before any backend call - reply push reuses NotificationEvent with a new admin_reply sub-kind - UI: /feedback route + screen, attachment picker, badge, channel detection, i18n - tests: feedback unit (Go+UI), gateway guest-gate, inttest lifecycle, e2e - docs: PLAN stage 19, ARCHITECTURE s15, FUNCTIONAL(+ru), TESTING, READMEs
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package feedback
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// maxAttachmentBytes caps a single attachment's raw size. Chosen to fit, with the
|
||||
// message text and the FlatBuffers framing, under the gateway's 1 MiB edge body
|
||||
// cap, so the whole submit request passes without weakening that cap.
|
||||
const maxAttachmentBytes = 1_000_000
|
||||
|
||||
// allowedExt is the attachment extension allow-list. It is mirrored on the UI as a
|
||||
// pre-upload gate; the server re-checks here as the trust boundary (metadata only,
|
||||
// the file content is never parsed). Images render inline in the console; the rest
|
||||
// are download-only.
|
||||
var allowedExt = map[string]bool{
|
||||
"png": true, "jpg": true, "jpeg": true, "webp": true, "gif": true, // images
|
||||
"pdf": true, "txt": true, "log": true, "doc": true, "docx": true,
|
||||
"rtf": true, "zip": true, "gz": true, "7z": true,
|
||||
}
|
||||
|
||||
// imageType maps an image extension to the content-type the console serves it with
|
||||
// (loaded only via <img>, which never executes, so a renamed non-image is inert).
|
||||
var imageType = map[string]string{
|
||||
"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
|
||||
"webp": "image/webp", "gif": "image/gif",
|
||||
}
|
||||
|
||||
// ext returns name's lower-cased extension without the leading dot.
|
||||
func ext(name string) string {
|
||||
return strings.ToLower(strings.TrimPrefix(filepath.Ext(name), "."))
|
||||
}
|
||||
|
||||
// AllowedAttachment reports whether name's extension is on the allow-list.
|
||||
func AllowedAttachment(name string) bool {
|
||||
return allowedExt[ext(name)]
|
||||
}
|
||||
|
||||
// IsImage reports whether name is an inline-previewable image by its extension.
|
||||
func IsImage(name string) bool {
|
||||
_, ok := imageType[ext(name)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ContentType returns the safe content-type the console serves the attachment
|
||||
// with: the matching image type for an image, else application/octet-stream so a
|
||||
// non-image is downloaded rather than rendered.
|
||||
func ContentType(name string) string {
|
||||
if t, ok := imageType[ext(name)]; ok {
|
||||
return t
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
Reference in New Issue
Block a user