feat: auth user feedbacks
This commit is contained in:
@@ -68,3 +68,21 @@ func (h *FeedbackHandler) Submit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
response.JSON(w, http.StatusCreated, f)
|
||||
}
|
||||
|
||||
// Mine returns the authenticated caller's own feedback submissions,
|
||||
// newest first — each with its status (acknowledged / resolved) and any
|
||||
// admin reply. Registered under RequireAuth, so UserID is always set;
|
||||
// the guard is belt-and-suspenders.
|
||||
func (h *FeedbackHandler) Mine(w http.ResponseWriter, r *http.Request) {
|
||||
uid := middleware.UserID(r.Context())
|
||||
if uid == "" {
|
||||
response.Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
items, err := h.svc.ListForUser(r.Context(), uid, 50)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "could not load your feedback")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, items)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,13 @@ type Feedback struct {
|
||||
// resolve is a no-op.
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||
|
||||
// ReplyMessage is the admin's written reply to the submitter, sent
|
||||
// (and stored) from the admin panel. NULL until an admin replies.
|
||||
// RepliedAt stamps when it was sent. Surfaced to the submitter on the
|
||||
// "My feedback" page as the team's response.
|
||||
ReplyMessage *string `json:"reply_message,omitempty"`
|
||||
RepliedAt *time.Time `json:"replied_at,omitempty"`
|
||||
|
||||
AuthorHandle *string `json:"author_handle,omitempty"`
|
||||
AuthorName *string `json:"author_name,omitempty"`
|
||||
AuthorEmail *string `json:"-"` // server-side only; never serialised
|
||||
|
||||
@@ -85,6 +85,39 @@ func (r *FeedbackRepo) MarkResolved(ctx context.Context, id string) (resolved bo
|
||||
return tag.RowsAffected() == 1, nil
|
||||
}
|
||||
|
||||
// ListByUser returns a single user's own feedback submissions, newest
|
||||
// first, with status timestamps and any admin reply. Backs the "My
|
||||
// feedback" page under /settings. No author joins — the caller IS the
|
||||
// author, so handle/name/email aren't needed here. Bounded limit keeps
|
||||
// the query cheap.
|
||||
func (r *FeedbackRepo) ListByUser(ctx context.Context, userID string, limit int) ([]model.Feedback, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := r.pg.Pool.Query(ctx, `
|
||||
SELECT id, user_id, category, message, contact_email, created_at,
|
||||
acknowledged_at, resolved_at, replied_at, reply_message
|
||||
FROM feedback
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2`, userID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query user feedback: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []model.Feedback{}
|
||||
for rows.Next() {
|
||||
var f model.Feedback
|
||||
if err := rows.Scan(&f.ID, &f.UserID, &f.Category, &f.Message, &f.ContactEmail, &f.CreatedAt,
|
||||
&f.AcknowledgedAt, &f.ResolvedAt, &f.RepliedAt, &f.ReplyMessage); err != nil {
|
||||
return nil, fmt.Errorf("scan user feedback: %w", err)
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Recent returns the most-recent N feedback rows joined with users
|
||||
// for display on /stats. NULL author columns mean anonymous (or the
|
||||
// original submitter's account was since deleted).
|
||||
|
||||
@@ -953,10 +953,20 @@ func (s *Server) setupRouter() {
|
||||
r.Group(func(r chi.Router) {
|
||||
if s.feedbackH == nil {
|
||||
r.HandleFunc("/feedback", s.authUnavailable)
|
||||
r.HandleFunc("/feedback/mine", s.authUnavailable)
|
||||
return
|
||||
}
|
||||
r.Use(middleware.OptionalAuth(s.authIssuer, s.userRepo))
|
||||
r.Post("/feedback", s.feedbackH.Submit)
|
||||
// Submit — OptionalAuth (anonymous callers allowed).
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.OptionalAuth(s.authIssuer, s.userRepo))
|
||||
r.Post("/feedback", s.feedbackH.Submit)
|
||||
})
|
||||
// My submissions — RequireAuth. Lists the caller's own rows
|
||||
// with status + admin reply for the /settings/feedbacks page.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.RequireAuth(s.authIssuer, s.userRepo))
|
||||
r.Get("/feedback/mine", s.feedbackH.Mine)
|
||||
})
|
||||
})
|
||||
|
||||
// Notifications — the signed-in user's in-app feed + unread badge.
|
||||
|
||||
@@ -92,6 +92,12 @@ func (s *FeedbackService) Submit(ctx context.Context, userID *string, category,
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ListForUser returns the signed-in user's own submissions (newest
|
||||
// first) with status and any admin reply, for the "My feedback" page.
|
||||
func (s *FeedbackService) ListForUser(ctx context.Context, userID string, limit int) ([]model.Feedback, error) {
|
||||
return s.repo.ListByUser(ctx, userID, limit)
|
||||
}
|
||||
|
||||
// ErrAlreadyResolved is returned by Resolve when the row was already
|
||||
// closed (or doesn't exist). The handler maps it to a benign redirect
|
||||
// rather than an error — the end state the admin wanted is already true.
|
||||
|
||||
Reference in New Issue
Block a user