293 lines
9.6 KiB
Go
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})
|
|
}
|