Files
2026-06-16 14:47:45 +05:30

293 lines
9.6 KiB
Go

package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi/v5"
"github.com/prabhupadasays/spsays-bff/internal/service"
"github.com/prabhupadasays/spsays-bff/pkg/response"
)
// AdminHandler serves /api/v1/admin/* — gated by RequireAuth + RequireAdmin in
// the router, so every method here can assume an admin caller.
type AdminHandler struct{ svc *service.AdminService }
func NewAdminHandler(svc *service.AdminService) *AdminHandler { return &AdminHandler{svc: svc} }
// pageParams pulls (q, limit, offset) from the query string with sane defaults.
func pageParams(r *http.Request) (q string, limit, offset int) {
q = strings.TrimSpace(r.URL.Query().Get("q"))
limit = 25
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 {
limit = v
}
offset = 0
if v, err := strconv.Atoi(r.URL.Query().Get("offset")); err == nil && v >= 0 {
offset = v
}
return q, limit, offset
}
// meta builds the pagination envelope metadata from a total + the request page.
func meta(total, limit, offset int) *response.Meta {
if limit <= 0 {
limit = 25
}
pages := (total + limit - 1) / limit
return &response.Meta{
Page: offset/limit + 1,
Limit: limit,
Total: int64(total),
TotalPages: pages,
}
}
func (h *AdminHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
q, limit, offset := pageParams(r)
items, total, err := h.svc.ListUsers(r.Context(), q, limit, offset)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not list users")
return
}
response.JSONWithMeta(w, http.StatusOK, items, meta(total, limit, offset))
}
func (h *AdminHandler) ListPosts(w http.ResponseWriter, r *http.Request) {
q, limit, offset := pageParams(r)
items, total, err := h.svc.ListPosts(r.Context(), q, limit, offset)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not list posts")
return
}
response.JSONWithMeta(w, http.StatusOK, items, meta(total, limit, offset))
}
func (h *AdminHandler) ListCollections(w http.ResponseWriter, r *http.Request) {
q, limit, offset := pageParams(r)
items, total, err := h.svc.ListCollections(r.Context(), q, limit, offset)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not list collections")
return
}
response.JSONWithMeta(w, http.StatusOK, items, meta(total, limit, offset))
}
func (h *AdminHandler) ListFeedback(w http.ResponseWriter, r *http.Request) {
_, limit, offset := pageParams(r)
status := strings.TrimSpace(r.URL.Query().Get("status"))
items, total, err := h.svc.ListFeedback(r.Context(), status, limit, offset)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not list feedback")
return
}
response.JSONWithMeta(w, http.StatusOK, items, meta(total, limit, offset))
}
func (h *AdminHandler) ListReports(w http.ResponseWriter, r *http.Request) {
q, limit, offset := pageParams(r)
reportType := strings.TrimSpace(r.URL.Query().Get("type"))
if reportType == "" {
reportType = "post"
}
if reportType != "post" && reportType != "comment" && reportType != "user" {
response.Error(w, http.StatusBadRequest, "type must be post, comment, or user")
return
}
items, total, err := h.svc.ListReports(r.Context(), reportType, q, limit, offset)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not list reports")
return
}
response.JSONWithMeta(w, http.StatusOK, items, meta(total, limit, offset))
}
// NotifyUser — POST /admin/users/{id}/notify { "title": "...", "body": "..." }
func (h *AdminHandler) NotifyUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var in struct {
Title string `json:"title"`
Body string `json:"body"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
response.Error(w, http.StatusBadRequest, "invalid request body")
return
}
in.Title = strings.TrimSpace(in.Title)
in.Body = strings.TrimSpace(in.Body)
if in.Body == "" {
response.Error(w, http.StatusBadRequest, "message body is required")
return
}
if len(in.Title) > 120 || len(in.Body) > 4000 {
response.Error(w, http.StatusBadRequest, "message too long")
return
}
err := h.svc.NotifyUser(r.Context(), id, in.Title, in.Body)
if errors.Is(err, service.ErrAdminUserNotFound) {
response.Error(w, http.StatusNotFound, "user not found")
return
}
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not send notification")
return
}
response.JSON(w, http.StatusOK, map[string]any{"sent": true})
}
// ReplyFeedback — POST /admin/feedback/{id}/reply { "message": "..." }
func (h *AdminHandler) ReplyFeedback(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var in struct {
Message string `json:"message"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
response.Error(w, http.StatusBadRequest, "invalid request body")
return
}
in.Message = strings.TrimSpace(in.Message)
if in.Message == "" {
response.Error(w, http.StatusBadRequest, "reply message is required")
return
}
if len(in.Message) > 4000 {
response.Error(w, http.StatusBadRequest, "reply too long")
return
}
found, emailed, err := h.svc.ReplyFeedback(r.Context(), id, in.Message)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not save reply")
return
}
if !found {
response.Error(w, http.StatusNotFound, "feedback not found")
return
}
response.JSON(w, http.StatusOK, map[string]any{"replied": true, "emailed": emailed})
}
// ResolveReport — POST /admin/reports/{type}/{id}/resolve
func (h *AdminHandler) ResolveReport(w http.ResponseWriter, r *http.Request) {
reportType := chi.URLParam(r, "type")
id := chi.URLParam(r, "id")
if reportType != "post" && reportType != "comment" && reportType != "user" {
response.Error(w, http.StatusBadRequest, "type must be post, comment, or user")
return
}
resolved, err := h.svc.ResolveReport(r.Context(), reportType, id)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not resolve report")
return
}
// resolved=false means it was already resolved or unknown — still 200 so
// the action is idempotent from the client's perspective.
response.JSON(w, http.StatusOK, map[string]any{"resolved": resolved})
}
// ─── Moderation actions ──────────────────────────────────────────────────────
// HidePost — POST /admin/posts/{id}/hide
func (h *AdminHandler) HidePost(w http.ResponseWriter, r *http.Request) {
h.setPostHidden(w, r, true)
}
// UnhidePost — DELETE /admin/posts/{id}/hide
func (h *AdminHandler) UnhidePost(w http.ResponseWriter, r *http.Request) {
h.setPostHidden(w, r, false)
}
func (h *AdminHandler) setPostHidden(w http.ResponseWriter, r *http.Request, hidden bool) {
ok, err := h.svc.SetPostHidden(r.Context(), chi.URLParam(r, "id"), hidden)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not update post")
return
}
if !ok {
response.Error(w, http.StatusNotFound, "post not found")
return
}
response.JSON(w, http.StatusOK, map[string]any{"hidden": hidden})
}
// DeletePost — DELETE /admin/posts/{id} (soft-delete; restorable)
func (h *AdminHandler) DeletePost(w http.ResponseWriter, r *http.Request) {
ok, err := h.svc.DeletePost(r.Context(), chi.URLParam(r, "id"))
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not delete post")
return
}
if !ok {
response.Error(w, http.StatusNotFound, "post not found")
return
}
response.JSON(w, http.StatusOK, map[string]any{"deleted": true})
}
// RestorePost — POST /admin/posts/{id}/restore
func (h *AdminHandler) RestorePost(w http.ResponseWriter, r *http.Request) {
ok, err := h.svc.RestorePost(r.Context(), chi.URLParam(r, "id"))
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not restore post")
return
}
if !ok {
response.Error(w, http.StatusNotFound, "post not found")
return
}
response.JSON(w, http.StatusOK, map[string]any{"restored": true})
}
// DeleteComment — DELETE /admin/comments/{id} (soft-delete; restorable)
func (h *AdminHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
ok, err := h.svc.DeleteComment(r.Context(), chi.URLParam(r, "id"))
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not delete comment")
return
}
if !ok {
response.Error(w, http.StatusNotFound, "comment not found")
return
}
response.JSON(w, http.StatusOK, map[string]any{"deleted": true})
}
// RestoreComment — POST /admin/comments/{id}/restore
func (h *AdminHandler) RestoreComment(w http.ResponseWriter, r *http.Request) {
ok, err := h.svc.RestoreComment(r.Context(), chi.URLParam(r, "id"))
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not restore comment")
return
}
if !ok {
response.Error(w, http.StatusNotFound, "comment not found")
return
}
response.JSON(w, http.StatusOK, map[string]any{"restored": true})
}
// BanUser — POST /admin/users/{id}/ban
func (h *AdminHandler) BanUser(w http.ResponseWriter, r *http.Request) {
h.setUserBanned(w, r, true)
}
// UnbanUser — DELETE /admin/users/{id}/ban
func (h *AdminHandler) UnbanUser(w http.ResponseWriter, r *http.Request) {
h.setUserBanned(w, r, false)
}
func (h *AdminHandler) setUserBanned(w http.ResponseWriter, r *http.Request, banned bool) {
ok, err := h.svc.SetUserBanned(r.Context(), chi.URLParam(r, "id"), banned)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not update account")
return
}
if !ok {
response.Error(w, http.StatusNotFound, "user not found")
return
}
response.JSON(w, http.StatusOK, map[string]any{"banned": banned})
}