419ea11b14
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
55 lines
1.9 KiB
Go
55 lines
1.9 KiB
Go
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"
|
|
}
|