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:
@@ -207,14 +207,16 @@ func (c *Client) EmailLogin(ctx context.Context, email, code string) (SessionRes
|
||||
return out, err
|
||||
}
|
||||
|
||||
// ResolveSession maps a token to its account id (gateway session-cache miss).
|
||||
func (c *Client) ResolveSession(ctx context.Context, token string) (string, error) {
|
||||
// ResolveSession maps a token to its account id and guest flag (gateway
|
||||
// session-cache miss). The guest flag lets the edge gate guest-forbidden ops.
|
||||
func (c *Client) ResolveSession(ctx context.Context, token string) (string, bool, error) {
|
||||
var out struct {
|
||||
UserID string `json:"user_id"`
|
||||
UserID string `json:"user_id"`
|
||||
IsGuest bool `json:"is_guest"`
|
||||
}
|
||||
err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/resolve", "", "",
|
||||
map[string]string{"token": token}, &out)
|
||||
return out.UserID, err
|
||||
return out.UserID, out.IsGuest, err
|
||||
}
|
||||
|
||||
// Profile returns the authenticated account's profile.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package backendclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FeedbackReplyResp is the operator reply shown back to the player.
|
||||
type FeedbackReplyResp struct {
|
||||
Body string `json:"body"`
|
||||
RepliedAtUnix int64 `json:"replied_at_unix"`
|
||||
}
|
||||
|
||||
// FeedbackStateResp is the player's feedback screen state. Reply is nil when there
|
||||
// is none to show.
|
||||
type FeedbackStateResp struct {
|
||||
CanSend bool `json:"can_send"`
|
||||
BlockedReason string `json:"blocked_reason"`
|
||||
Reply *FeedbackReplyResp `json:"reply,omitempty"`
|
||||
}
|
||||
|
||||
// FeedbackUnreadResp reports whether the player has an undelivered operator reply.
|
||||
type FeedbackUnreadResp struct {
|
||||
ReplyUnread bool `json:"reply_unread"`
|
||||
}
|
||||
|
||||
// FeedbackSubmit posts a feedback message. The attachment bytes are base64-encoded
|
||||
// into the JSON body for the internal hop; clientIP rides X-Forwarded-For.
|
||||
func (c *Client) FeedbackSubmit(ctx context.Context, userID, body string, attachment []byte, attachmentName, channel, clientIP string) error {
|
||||
payload := map[string]string{
|
||||
"body": body,
|
||||
"attachment": "",
|
||||
"attachment_name": attachmentName,
|
||||
"channel": channel,
|
||||
}
|
||||
if len(attachment) > 0 {
|
||||
payload["attachment"] = base64.StdEncoding.EncodeToString(attachment)
|
||||
}
|
||||
return c.do(ctx, http.MethodPost, "/api/v1/user/feedback", userID, clientIP, payload, nil)
|
||||
}
|
||||
|
||||
// FeedbackGet returns the player's feedback screen state, marking any pending reply
|
||||
// delivered (clearing the badge).
|
||||
func (c *Client) FeedbackGet(ctx context.Context, userID string) (FeedbackStateResp, error) {
|
||||
var out FeedbackStateResp
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/user/feedback", userID, "", nil, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// FeedbackUnread reports whether the player has an undelivered operator reply.
|
||||
func (c *Client) FeedbackUnread(ctx context.Context, userID string) (FeedbackUnreadResp, error) {
|
||||
var out FeedbackUnreadResp
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/user/feedback/unread", userID, "", nil, &out)
|
||||
return out, err
|
||||
}
|
||||
Reference in New Issue
Block a user