1dc96e86fc
- 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.
449 lines
14 KiB
Go
449 lines
14 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"dating-app/internal/dto"
|
|
"dating-app/internal/models"
|
|
"dating-app/internal/repository"
|
|
"dating-app/internal/service"
|
|
"dating-app/internal/utils"
|
|
)
|
|
|
|
type AdminHandler struct {
|
|
adminService *service.AdminService
|
|
optionRepo *repository.OptionRepository
|
|
questionRepo *repository.QuestionRepository
|
|
photoRepo *repository.PhotoRepository
|
|
onboardingService *service.OnboardingService
|
|
}
|
|
|
|
func NewAdminHandler(
|
|
adminService *service.AdminService,
|
|
optionRepo *repository.OptionRepository,
|
|
questionRepo *repository.QuestionRepository,
|
|
photoRepo *repository.PhotoRepository,
|
|
onboardingService *service.OnboardingService,
|
|
) *AdminHandler {
|
|
return &AdminHandler{
|
|
adminService: adminService,
|
|
optionRepo: optionRepo,
|
|
questionRepo: questionRepo,
|
|
photoRepo: photoRepo,
|
|
onboardingService: onboardingService,
|
|
}
|
|
}
|
|
|
|
func validCategoryKey(key string) (models.CategoryKey, bool) {
|
|
k := models.CategoryKey(key)
|
|
for _, valid := range models.AllCategoryKeys {
|
|
if valid == k {
|
|
return k, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// ---------------- دستهها ----------------
|
|
|
|
// GetCategory GET /admin/categories/:key
|
|
func (h *AdminHandler) GetCategory(c *gin.Context) {
|
|
key, ok := validCategoryKey(c.Param("key"))
|
|
if !ok {
|
|
utils.Fail(c, http.StatusNotFound, "دستهی نامعتبر است", nil)
|
|
return
|
|
}
|
|
cat, err := h.adminService.GetCategory(key)
|
|
if err != nil {
|
|
utils.Fail(c, http.StatusNotFound, "دسته یافت نشد", nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "", cat)
|
|
}
|
|
|
|
// UpdateCategory PATCH /admin/categories/:key
|
|
func (h *AdminHandler) UpdateCategory(c *gin.Context) {
|
|
key, ok := validCategoryKey(c.Param("key"))
|
|
if !ok {
|
|
utils.Fail(c, http.StatusNotFound, "دستهی نامعتبر است", nil)
|
|
return
|
|
}
|
|
var req dto.UpdateCategoryRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
|
|
return
|
|
}
|
|
cat, err := h.adminService.UpdateCategory(key, req)
|
|
if err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "دسته بهروزرسانی شد", cat)
|
|
}
|
|
|
|
// ---------------- گزینهها ----------------
|
|
|
|
// ListOptions GET /admin/categories/:key/options
|
|
func (h *AdminHandler) ListOptions(c *gin.Context) {
|
|
key, ok := validCategoryKey(c.Param("key"))
|
|
if !ok {
|
|
utils.Fail(c, http.StatusNotFound, "دستهی نامعتبر است", nil)
|
|
return
|
|
}
|
|
options, err := h.adminService.ListOptions(key)
|
|
if err != nil {
|
|
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت گزینهها", nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "", options)
|
|
}
|
|
|
|
// CreateOption POST /admin/categories/:key/options
|
|
func (h *AdminHandler) CreateOption(c *gin.Context) {
|
|
key, ok := validCategoryKey(c.Param("key"))
|
|
if !ok {
|
|
utils.Fail(c, http.StatusNotFound, "دستهی نامعتبر است", nil)
|
|
return
|
|
}
|
|
var req dto.CreateOptionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
|
|
return
|
|
}
|
|
opt, err := h.adminService.CreateOption(key, req)
|
|
if err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusCreated, "گزینه ساخته شد", opt)
|
|
}
|
|
|
|
// UpdateOption PATCH /admin/options/:id
|
|
func (h *AdminHandler) UpdateOption(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.UpdateOptionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
|
|
return
|
|
}
|
|
opt, err := h.adminService.UpdateOption(uint(id), req)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.Fail(c, http.StatusNotFound, "گزینه یافت نشد", nil)
|
|
return
|
|
}
|
|
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "گزینه بهروزرسانی شد", opt)
|
|
}
|
|
|
|
// DeleteOption DELETE /admin/options/:id
|
|
func (h *AdminHandler) DeleteOption(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.DeleteOption(uint(id)); err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, "حذف گزینه با خطا مواجه شد", nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "گزینه حذف شد", nil)
|
|
}
|
|
|
|
// ---------------- بررسی و تایید کاربران ----------------
|
|
|
|
// ListPendingUsers GET /admin/users/pending
|
|
func (h *AdminHandler) ListPendingUsers(c *gin.Context) {
|
|
users, err := h.adminService.ListPendingUsers()
|
|
if err != nil {
|
|
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت لیست کاربران", nil)
|
|
return
|
|
}
|
|
result := make([]dto.UserProfile, 0, len(users))
|
|
for i := range users {
|
|
result = append(result, toUserProfile(&users[i]))
|
|
}
|
|
utils.Success(c, http.StatusOK, "", result)
|
|
}
|
|
|
|
// GetUserDetail GET /admin/users/:id — پروفایل کامل یک کاربر برای بررسی ادمین
|
|
func (h *AdminHandler) GetUserDetail(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.GetUser(uint(id))
|
|
if err != nil {
|
|
utils.Fail(c, http.StatusNotFound, "کاربر یافت نشد", nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "", fullProfilePayload(user, h.optionRepo, h.questionRepo, h.onboardingService, h.photoRepo))
|
|
}
|
|
|
|
// ApproveUser POST /admin/users/:id/approve
|
|
func (h *AdminHandler) ApproveUser(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.ApproveUser(uint(id))
|
|
if err != nil {
|
|
h.handleUserReviewError(c, err)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "کاربر تایید شد", toUserProfile(user))
|
|
}
|
|
|
|
// RejectUser POST /admin/users/:id/reject body: {reason}
|
|
func (h *AdminHandler) RejectUser(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.RejectUserRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است (ذکر دلیل رد الزامی است)", err.Error())
|
|
return
|
|
}
|
|
user, err := h.adminService.RejectUser(uint(id), req.Reason)
|
|
if err != nil {
|
|
h.handleUserReviewError(c, err)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "کاربر رد شد", toUserProfile(user))
|
|
}
|
|
|
|
func (h *AdminHandler) handleUserReviewError(c *gin.Context, err error) {
|
|
switch {
|
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
|
utils.Fail(c, http.StatusNotFound, "کاربر یافت نشد", nil)
|
|
case errors.Is(err, service.ErrUserNotOnboarded):
|
|
utils.Fail(c, http.StatusConflict, err.Error(), nil)
|
|
default:
|
|
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
func (h *AdminHandler) ListQuestions(c *gin.Context) {
|
|
qs, err := h.adminService.ListQuestions()
|
|
if err != nil {
|
|
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت سوالات", nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "", qs)
|
|
}
|
|
|
|
// CreateQuestion POST /admin/profile-questions
|
|
func (h *AdminHandler) CreateQuestion(c *gin.Context) {
|
|
var req dto.CreateProfileQuestionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
|
|
return
|
|
}
|
|
q, err := h.adminService.CreateQuestion(req)
|
|
if err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusCreated, "سوال ساخته شد", q)
|
|
}
|
|
|
|
// UpdateQuestion PATCH /admin/profile-questions/:id
|
|
func (h *AdminHandler) UpdateQuestion(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.UpdateProfileQuestionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
|
|
return
|
|
}
|
|
q, err := h.adminService.UpdateQuestion(uint(id), req)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.Fail(c, http.StatusNotFound, "سوال یافت نشد", nil)
|
|
return
|
|
}
|
|
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "سوال بهروزرسانی شد", q)
|
|
}
|
|
|
|
// DeleteQuestion DELETE /admin/profile-questions/:id
|
|
func (h *AdminHandler) DeleteQuestion(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.DeleteQuestion(uint(id)); err != nil {
|
|
utils.Fail(c, http.StatusBadRequest, "حذف سوال با خطا مواجه شد", nil)
|
|
return
|
|
}
|
|
utils.Success(c, http.StatusOK, "سوال حذف شد", nil)
|
|
}
|