feat: auth user feedbacks

This commit is contained in:
Krispire Admin
2026-07-14 18:26:03 +05:30
parent 47b3a6d96e
commit 4553725d4e
5 changed files with 76 additions and 2 deletions
+18
View File
@@ -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)
}
+7
View File
@@ -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
+33
View File
@@ -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).
+12 -2
View File
@@ -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.
+6
View File
@@ -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.