Add admin user moderation: block/unblock, reports, matches list, photo review

- User blocking (with reason) now closes the auth gap immediately — a
  blocked user's still-valid access token stops working on the next
  request, not just on their next OTP/refresh.
- Admin-created reports (moderation notes) per user, viewable in a history
  list independent of block status.
- GET /admin/users: full paginated, searchable user list with report counts.
- GET /admin/matches: all matches across the app ("friends" who mutually
  liked each other), for admin visibility.
- GET/DELETE /admin/photos: paginated feed of every uploaded photo with
  uploader info, so admins can review and remove content without going
  user-by-user.
- Per-user activity: monitoring requests can now be filtered by user_id, so
  a user's detail page can show their recent API activity.
- Batched the profile-selections query in fullProfilePayload (was 6 queries
  per view, now 1) for consistency with the earlier N+1 fixes.
This commit is contained in:
2026-09-20 05:30:14 +03:30
parent 769e76313f
commit 1dc96e86fc
19 changed files with 628 additions and 25 deletions
+144
View File
@@ -238,6 +238,150 @@ func (h *AdminHandler) handleUserReviewError(c *gin.Context, err error) {
}
}
// paginationParams صفحه/تعداد رو از کوئری استرینگ می‌خونه (پیش‌فرض‌های معقول اگه نبود/نامعتبر بود)
func paginationParams(c *gin.Context, defaultSize int) (page, pageSize int) {
page = 1
if p, err := strconv.Atoi(c.Query("page")); err == nil && p > 0 {
page = p
}
pageSize = defaultSize
if ps, err := strconv.Atoi(c.Query("page_size")); err == nil && ps > 0 {
pageSize = ps
}
return
}
// ---------------- همه‌ی کاربران، بلاک/آنبلاک، گزارش ----------------
// ListAllUsers GET /admin/users?search=&page=&page_size=
func (h *AdminHandler) ListAllUsers(c *gin.Context) {
page, pageSize := paginationParams(c, 25)
search := c.Query("search")
items, total, err := h.adminService.ListAllUsers(search, page, pageSize)
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت لیست کاربران", nil)
return
}
utils.Success(c, http.StatusOK, "", gin.H{
"items": items, "total": total, "page": page, "page_size": pageSize,
})
}
// BlockUser POST /admin/users/:id/block body: {reason}
func (h *AdminHandler) BlockUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
var req dto.BlockUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است (ذکر دلیل بلاک الزامی است)", err.Error())
return
}
user, err := h.adminService.BlockUser(uint(id), req.Reason)
if err != nil {
h.handleUserReviewError(c, err)
return
}
utils.Success(c, http.StatusOK, "کاربر بلاک شد", toUserProfile(user))
}
// UnblockUser POST /admin/users/:id/unblock
func (h *AdminHandler) UnblockUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
user, err := h.adminService.UnblockUser(uint(id))
if err != nil {
h.handleUserReviewError(c, err)
return
}
utils.Success(c, http.StatusOK, "کاربر آنبلاک شد", toUserProfile(user))
}
// ReportUser POST /admin/users/:id/report body: {reason}
func (h *AdminHandler) ReportUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
var req dto.ReportUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است (ذکر دلیل گزارش الزامی است)", err.Error())
return
}
report, err := h.adminService.ReportUser(uint(id), req.Reason)
if err != nil {
h.handleUserReviewError(c, err)
return
}
utils.Success(c, http.StatusCreated, "گزارش ثبت شد", report)
}
// ListUserReports GET /admin/users/:id/reports
func (h *AdminHandler) ListUserReports(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
reports, err := h.adminService.ListReports(uint(id))
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت گزارش‌ها", nil)
return
}
utils.Success(c, http.StatusOK, "", reports)
}
// ---------------- مچ‌ها (رفقا) ----------------
// ListAllMatches GET /admin/matches?page=&page_size=
func (h *AdminHandler) ListAllMatches(c *gin.Context) {
page, pageSize := paginationParams(c, 25)
items, total, err := h.adminService.ListAllMatches(page, pageSize)
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت لیست مچ‌ها", nil)
return
}
utils.Success(c, http.StatusOK, "", gin.H{
"items": items, "total": total, "page": page, "page_size": pageSize,
})
}
// ---------------- نظارت بر عکس‌ها ----------------
// ListAllPhotos GET /admin/photos?page=&page_size=
func (h *AdminHandler) ListAllPhotos(c *gin.Context) {
page, pageSize := paginationParams(c, 50)
items, total, err := h.adminService.ListAllPhotos(page, pageSize)
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت عکس‌ها", nil)
return
}
utils.Success(c, http.StatusOK, "", gin.H{
"items": items, "total": total, "page": page, "page_size": pageSize,
})
}
// DeletePhoto DELETE /admin/photos/:id
func (h *AdminHandler) DeletePhoto(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
if err := h.adminService.DeletePhotoAdmin(uint(id)); err != nil {
utils.Fail(c, http.StatusNotFound, "عکس یافت نشد", nil)
return
}
utils.Success(c, http.StatusOK, "عکس حذف شد", nil)
}
// ---------------- سوالات پروفایل ----------------
// ListQuestions GET /admin/profile-questions