chore: merge backend repo history into monorepo at backend/

git-subtree-dir: backend
git-subtree-mainline: dc11190527
git-subtree-split: acae976622
This commit is contained in:
2026-09-27 10:30:16 +03:30
67 changed files with 6835 additions and 0 deletions
+479
View File
@@ -0,0 +1,479 @@
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, photosByUser, err := h.adminService.ListPendingUsers()
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت لیست کاربران", nil)
return
}
result := make([]dto.UserProfileWithPhotos, 0, len(users))
for i := range users {
photos := photosByUser[users[i].ID]
if photos == nil {
photos = []string{}
}
result = append(result, dto.UserProfileWithPhotos{
UserProfile: toUserProfile(&users[i]),
Photos: photos,
})
}
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)
}
// ReportPhoto POST /admin/photos/:id/report body: {reason}
func (h *AdminHandler) ReportPhoto(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.ReportPhoto(uint(id), req.Reason)
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.StatusCreated, "گزارش ثبت شد", report)
}
// ---------------- سوالات پروفایل ----------------
// 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)
}
+100
View File
@@ -0,0 +1,100 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"dating-app/internal/dto"
"dating-app/internal/service"
"dating-app/internal/utils"
)
type AuthHandler struct {
authService *service.AuthService
}
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
// RequestOTP POST /auth/request-otp
func (h *AuthHandler) RequestOTP(c *gin.Context) {
var req dto.RequestOTPRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
err := h.authService.RequestOTP(req.Phone)
if err != nil {
var stillValid *service.OTPStillValidError
switch {
case errors.As(err, &stillValid):
utils.Fail(c, http.StatusTooManyRequests,
"کد تایید قبلی هنوز معتبر است", gin.H{"remaining_seconds": stillValid.RemainingSeconds})
case errors.Is(err, service.ErrPhoneBlocked):
utils.Fail(c, http.StatusForbidden, err.Error(), nil)
case errors.Is(err, service.ErrSMSSendFailed):
utils.Fail(c, http.StatusServiceUnavailable, err.Error(), nil)
default:
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
}
return
}
utils.Success(c, http.StatusOK, "کد تایید با موفقیت ارسال شد", nil)
}
// VerifyOTP POST /auth/verify-otp
func (h *AuthHandler) VerifyOTP(c *gin.Context) {
var req dto.VerifyOTPRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
user, access, refresh, isNew, err := h.authService.VerifyOTP(req.Phone, req.OTP)
if err != nil {
switch {
case errors.Is(err, service.ErrUserNotFound):
utils.Fail(c, http.StatusNotFound, err.Error(), nil)
case errors.Is(err, service.ErrOTPExpired), errors.Is(err, service.ErrOTPInvalid), errors.Is(err, service.ErrOTPNotRequested):
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
default:
utils.Fail(c, http.StatusInternalServerError, "خطای داخلی سرور", nil)
}
return
}
utils.Success(c, http.StatusOK, "ورود با موفقیت انجام شد", dto.AuthResponse{
Tokens: dto.TokenPair{
AccessToken: access,
RefreshToken: refresh,
},
User: toUserProfile(user),
IsNewUser: isNew,
OnboardingDone: user.IsOnboarded,
})
}
// RefreshToken POST /auth/token/refresh
func (h *AuthHandler) RefreshToken(c *gin.Context) {
var req dto.RefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
access, refresh, err := h.authService.RefreshTokens(req.RefreshToken)
if err != nil {
utils.Fail(c, http.StatusUnauthorized, "رفرش توکن نامعتبر یا منقضی شده است", nil)
return
}
utils.Success(c, http.StatusOK, "توکن جدید صادر شد", dto.TokenPair{
AccessToken: access,
RefreshToken: refresh,
})
}
@@ -0,0 +1,49 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"dating-app/internal/service"
"dating-app/internal/utils"
)
type BackupHandler struct {
backupService *service.BackupService
}
func NewBackupHandler(backupService *service.BackupService) *BackupHandler {
return &BackupHandler{backupService: backupService}
}
// List GET /admin/backups
func (h *BackupHandler) List(c *gin.Context) {
backups, err := h.backupService.List()
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت لیست بک‌آپ‌ها", nil)
return
}
utils.Success(c, http.StatusOK, "", backups)
}
// Create POST /admin/backups
func (h *BackupHandler) Create(c *gin.Context) {
info, err := h.backupService.Create()
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "گرفتن بک‌آپ ناموفق بود: "+err.Error(), nil)
return
}
utils.Success(c, http.StatusCreated, "بک‌آپ با موفقیت ساخته شد", info)
}
// Download GET /admin/backups/:filename
func (h *BackupHandler) Download(c *gin.Context) {
filename := c.Param("filename")
path, err := h.backupService.ResolvePath(filename)
if err != nil {
utils.Fail(c, http.StatusNotFound, "فایل بک‌آپ یافت نشد", nil)
return
}
c.FileAttachment(path, filename)
}
+91
View File
@@ -0,0 +1,91 @@
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"dating-app/internal/dto"
"dating-app/internal/service"
"dating-app/internal/utils"
)
type ChatHandler struct {
chatService *service.ChatService
}
func NewChatHandler(chatService *service.ChatService) *ChatHandler {
return &ChatHandler{chatService: chatService}
}
// ListMatches GET /matches
func (h *ChatHandler) ListMatches(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
matches, err := h.chatService.ListMatches(user.ID)
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت مچ‌ها", nil)
return
}
utils.Success(c, http.StatusOK, "", matches)
}
// ListMessages GET /matches/:id/messages
func (h *ChatHandler) ListMessages(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
matchID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
messages, err := h.chatService.ListMessages(user.ID, uint(matchID))
if err != nil {
h.handleError(c, err)
return
}
utils.Success(c, http.StatusOK, "", messages)
}
// SendMessage POST /matches/:id/messages body: {cipher_text, nonce}
// متن خام پیام هیچ‌وقت اینجا نمی‌رسه؛ کلاینت قبل از ارسال با کلید عمومی
// طرف مقابل (NaCl box / X25519) رمزنگاریش کرده.
func (h *ChatHandler) SendMessage(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
matchID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
var req dto.SendMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "پیام رمزنگاری‌شده نامعتبر است", err.Error())
return
}
msg, err := h.chatService.SendMessage(user.ID, uint(matchID), req.CipherText, req.Nonce)
if err != nil {
h.handleError(c, err)
return
}
utils.Success(c, http.StatusCreated, "", msg)
}
func (h *ChatHandler) handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrNotMatchMember):
utils.Fail(c, http.StatusForbidden, err.Error(), nil)
case errors.Is(err, service.ErrInvalidMessageEncoding):
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
default:
utils.Fail(c, http.StatusNotFound, "گفتگو یافت نشد", nil)
}
}
+118
View File
@@ -0,0 +1,118 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"dating-app/internal/dto"
"dating-app/internal/middleware"
"dating-app/internal/models"
"dating-app/internal/repository"
"dating-app/internal/service"
"dating-app/internal/utils"
)
// currentUser کاربر جاری رو از context (که توسط AuthRequired ست شده) استخراج می‌کنه
func currentUser(c *gin.Context) (*models.User, bool) {
val, exists := c.Get(middleware.ContextUserKey)
if !exists {
utils.Fail(c, http.StatusUnauthorized, "احراز هویت لازم است", nil)
return nil, false
}
user, ok := val.(*models.User)
if !ok {
utils.Fail(c, http.StatusInternalServerError, "خطای داخلی سرور", nil)
return nil, false
}
return user, true
}
func toUserProfile(u *models.User) dto.UserProfile {
profile := dto.UserProfile{
ID: u.ID,
Phone: u.Phone,
FirstName: u.FirstName,
CurrentStep: int(u.CurrentStep),
IsOnboarded: u.IsOnboarded,
ApprovalStatus: string(u.ApprovalStatus),
RejectionReason: u.RejectionReason,
IsAdmin: u.IsAdmin,
HeightCM: u.HeightCM,
WeightKG: u.WeightKG,
State: string(u.State),
BlockedReason: u.BlockedReason,
BlockedAt: u.BlockedAt,
Bio: u.Bio,
StreakCount: u.StreakCount,
NotifyMatches: u.NotifyMatches,
NotifyMessages: u.NotifyMessages,
NotifyLikes: u.NotifyLikes,
}
if u.Gender != nil {
g := string(*u.Gender)
profile.Gender = &g
}
if u.BirthDate != nil {
bd := u.BirthDate.Format("2006-01-02")
profile.BirthDate = &bd
}
return profile
}
// selectionSummary یک دسته‌ی انتخابی همراه با عنوان گزینه‌های انتخاب‌شده
type selectionSummary struct {
CategoryKey string `json:"category_key"`
OptionIDs []uint `json:"option_ids"`
Titles []string `json:"titles"`
}
// fullProfilePayload پروفایل کامل یک کاربر (اطلاعات پایه + همه‌ی انتخاب‌ها/پاسخ‌ها)
// رو می‌سازه. هم توسط خود کاربر (GET /profile/me) و هم توسط ادمین موقع بررسی
// درخواست تایید (GET /admin/users/:id) استفاده می‌شه.
func fullProfilePayload(
user *models.User,
optionRepo *repository.OptionRepository,
questionRepo *repository.QuestionRepository,
onboardingService *service.OnboardingService,
photoRepo *repository.PhotoRepository,
) gin.H {
// دسته‌ها از کش خونده می‌شن (تغییر به‌ندرت اتفاق می‌افته)، ولی خودِ
// انتخاب‌های کاربر توی همه‌ی دسته‌ها با یک کوئری واحد گرفته می‌شه؛ قبلاً
// اینجا به‌ازای هر ۶ دسته یک کوئری جدا زده می‌شد.
keyToCategoryID := make(map[models.CategoryKey]uint, len(models.AllCategoryKeys))
categoryIDs := make([]uint, 0, len(models.AllCategoryKeys))
for _, key := range models.AllCategoryKeys {
cat, err := optionRepo.GetCategoryByKey(key)
if err != nil {
continue
}
keyToCategoryID[key] = cat.ID
categoryIDs = append(categoryIDs, cat.ID)
}
selectionsByCategoryID, _ := optionRepo.GetUserSelectionsForCategories(user.ID, categoryIDs)
selections := make([]selectionSummary, 0, len(models.AllCategoryKeys))
for _, key := range models.AllCategoryKeys {
summary := selectionSummary{CategoryKey: string(key)}
for _, s := range selectionsByCategoryID[keyToCategoryID[key]] {
summary.OptionIDs = append(summary.OptionIDs, s.OptionID)
if s.Option != nil {
summary.Titles = append(summary.Titles, s.Option.Title)
}
}
selections = append(selections, summary)
}
answers, _ := questionRepo.GetUserAnswers(user.ID)
photos, _ := photoRepo.ListByUser(user.ID)
return gin.H{
"user": toUserProfile(user),
"onboarding_status": onboardingService.GetStatus(user),
"selections": selections,
"profile_answers": answers,
"photos": photos,
}
}
@@ -0,0 +1,101 @@
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"dating-app/internal/dto"
"dating-app/internal/models"
"dating-app/internal/service"
"dating-app/internal/utils"
)
type DiscoveryHandler struct {
discoveryService *service.DiscoveryService
}
func NewDiscoveryHandler(discoveryService *service.DiscoveryService) *DiscoveryHandler {
return &DiscoveryHandler{discoveryService: discoveryService}
}
// Feed GET /discover?max_distance_km=&min_age=&max_age=
func (h *DiscoveryHandler) Feed(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var maxDistance *float64
if raw := c.Query("max_distance_km"); raw != "" {
if v, err := strconv.ParseFloat(raw, 64); err == nil {
maxDistance = &v
}
}
minAge, _ := strconv.Atoi(c.Query("min_age"))
maxAge, _ := strconv.Atoi(c.Query("max_age"))
candidates, err := h.discoveryService.GetFeed(user, maxDistance, minAge, maxAge)
if err != nil {
h.handleError(c, err)
return
}
utils.Success(c, http.StatusOK, "", candidates)
}
// CandidateDetail GET /discover/:id
func (h *DiscoveryHandler) CandidateDetail(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
targetID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
detail, err := h.discoveryService.GetCandidateDetail(user, uint(targetID))
if err != nil {
utils.Fail(c, http.StatusNotFound, "پروفایل یافت نشد", nil)
return
}
utils.Success(c, http.StatusOK, "", detail)
}
// Swipe POST /discover/:id/swipe body: {action: "like"|"pass"|"superlike"}
func (h *DiscoveryHandler) Swipe(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
targetID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
var req dto.SwipeRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
result, err := h.discoveryService.Swipe(user, uint(targetID), models.SwipeAction(req.Action))
if err != nil {
h.handleError(c, err)
return
}
utils.Success(c, http.StatusOK, "", result)
}
func (h *DiscoveryHandler) handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrGenderNotSet):
utils.Fail(c, http.StatusConflict, err.Error(), nil)
case errors.Is(err, service.ErrCannotSwipeSelf), errors.Is(err, service.ErrAlreadySwiped):
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
default:
utils.Fail(c, http.StatusInternalServerError, "خطای داخلی سرور", nil)
}
}
@@ -0,0 +1,45 @@
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"dating-app/internal/monitoring"
"dating-app/internal/utils"
)
type MonitoringHandler struct {
store *monitoring.Store
}
func NewMonitoringHandler(store *monitoring.Store) *MonitoringHandler {
return &MonitoringHandler{store: store}
}
// Summary GET /admin/monitoring/summary
func (h *MonitoringHandler) Summary(c *gin.Context) {
utils.Success(c, http.StatusOK, "", h.store.Summary())
}
// RecentRequests GET /admin/monitoring/requests?limit=100&user_id=42
// اگه user_id داده بشه، فقط فعالیت اخیر همون کاربر برمی‌گرده (برای صفحه‌ی
// جزئیات کاربر توی پنل ادمین)؛ وگرنه آخرین درخواست‌های کل سرور.
func (h *MonitoringHandler) RecentRequests(c *gin.Context) {
limit := 100
if raw := c.Query("limit"); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n > 0 && n <= 1000 {
limit = n
}
}
if raw := c.Query("user_id"); raw != "" {
if uid, err := strconv.ParseUint(raw, 10, 64); err == nil {
utils.Success(c, http.StatusOK, "", h.store.RecentByUser(uint(uid), limit))
return
}
}
utils.Success(c, http.StatusOK, "", h.store.Recent(limit))
}
@@ -0,0 +1,241 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"dating-app/internal/dto"
"dating-app/internal/models"
"dating-app/internal/repository"
"dating-app/internal/service"
"dating-app/internal/utils"
)
type OnboardingHandler struct {
onboardingService *service.OnboardingService
}
func NewOnboardingHandler(onboardingService *service.OnboardingService) *OnboardingHandler {
return &OnboardingHandler{onboardingService: onboardingService}
}
func (h *OnboardingHandler) handleStepError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrStepOutOfOrder):
utils.Fail(c, http.StatusConflict, err.Error(), nil)
case errors.Is(err, service.ErrInvalidBirthDate),
errors.Is(err, service.ErrOptionsNotFound),
errors.Is(err, service.ErrQuestionsInvalid),
errors.Is(err, service.ErrDuplicateQuestion):
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
case errors.Is(err, repository.ErrCategoryNotFound):
utils.Fail(c, http.StatusInternalServerError, "دسته‌ی موردنیاز در سیستم تعریف نشده است", nil)
case errors.Is(err, service.ErrSelectionCount):
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
default:
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
}
}
// Status GET /onboarding/status
func (h *OnboardingHandler) Status(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
utils.Success(c, http.StatusOK, "", h.onboardingService.GetStatus(user))
}
// BasicInfo POST /onboarding/basic-info
func (h *OnboardingHandler) BasicInfo(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.BasicInfoRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.onboardingService.SetBasicInfo(user, req); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "اطلاعات پایه ثبت شد", h.onboardingService.GetStatus(user))
}
// Photos POST /onboarding/photos — تایید مرحله‌ی عکس‌ها (خودِ آپلود از /profile/photos انجام می‌شه)
func (h *OnboardingHandler) Photos(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
if err := h.onboardingService.ConfirmPhotos(user); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "عکس‌ها ثبت شد", h.onboardingService.GetStatus(user))
}
// BodyInfo POST /onboarding/body-info
func (h *OnboardingHandler) BodyInfo(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.BodyInfoRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.onboardingService.SetBodyInfo(user, req); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "قد و وزن ثبت شد", h.onboardingService.GetStatus(user))
}
// AvailableQuestions GET /onboarding/profile-questions
func (h *OnboardingHandler) AvailableQuestions(c *gin.Context) {
questions, err := h.onboardingService.GetAvailableQuestions()
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت سوالات", nil)
return
}
utils.Success(c, http.StatusOK, "", questions)
}
// SetProfileQuestions POST /onboarding/profile-questions
func (h *OnboardingHandler) SetProfileQuestions(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.ProfileQuestionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است (باید دقیقاً ۳ سوال با پاسخ ارسال شود)", err.Error())
return
}
if err := h.onboardingService.SetProfileQuestions(user, req); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "پاسخ‌ها ثبت شد", h.onboardingService.GetStatus(user))
}
// Education POST /onboarding/education
func (h *OnboardingHandler) Education(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.EducationRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.onboardingService.SetEducation(user, req); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "اطلاعات تحصیلی ثبت شد", h.onboardingService.GetStatus(user))
}
// Occupations POST /onboarding/occupations
func (h *OnboardingHandler) Occupations(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.OptionSelectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.onboardingService.SetOccupations(user, req.OptionIDs); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "فعالیت‌ها ثبت شد", h.onboardingService.GetStatus(user))
}
// RelationshipImportance POST /onboarding/relationship-importance
func (h *OnboardingHandler) RelationshipImportance(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.OptionSelectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.onboardingService.SetRelationshipImportance(user, req.OptionIDs); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "موارد مهم در رابطه ثبت شد", h.onboardingService.GetStatus(user))
}
// Interests POST /onboarding/interests
func (h *OnboardingHandler) Interests(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.OptionSelectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.onboardingService.SetInterests(user, req.OptionIDs); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "علاقه‌مندی‌ها ثبت شد", h.onboardingService.GetStatus(user))
}
// LookingFor POST /onboarding/looking-for
func (h *OnboardingHandler) LookingFor(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.OptionSelectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.onboardingService.SetLookingFor(user, req.OptionIDs); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "هدف شما ثبت شد", h.onboardingService.GetStatus(user))
}
// Options GET /onboarding/options/:category
// category یکی از: education_level, field_of_study, occupation,
// important_in_relationship, interest, looking_for
func (h *OnboardingHandler) Options(c *gin.Context) {
key := models.CategoryKey(c.Param("category"))
valid := false
for _, k := range models.AllCategoryKeys {
if k == key {
valid = true
break
}
}
if !valid {
utils.Fail(c, http.StatusNotFound, "دسته‌ی نامعتبر است", nil)
return
}
options, err := h.onboardingService.GetActiveOptions(key)
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت گزینه‌ها", nil)
return
}
utils.Success(c, http.StatusOK, "", options)
}
+227
View File
@@ -0,0 +1,227 @@
package handler
import (
"errors"
"io"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"dating-app/internal/dto"
"dating-app/internal/repository"
"dating-app/internal/service"
"dating-app/internal/utils"
)
type ProfileHandler struct {
optionRepo *repository.OptionRepository
questionRepo *repository.QuestionRepository
photoRepo *repository.PhotoRepository
onboardingService *service.OnboardingService
profileService *service.ProfileService
}
func NewProfileHandler(
optionRepo *repository.OptionRepository,
questionRepo *repository.QuestionRepository,
photoRepo *repository.PhotoRepository,
onboardingService *service.OnboardingService,
profileService *service.ProfileService,
) *ProfileHandler {
return &ProfileHandler{
optionRepo: optionRepo,
questionRepo: questionRepo,
photoRepo: photoRepo,
onboardingService: onboardingService,
profileService: profileService,
}
}
// Me GET /profile/me — پروفایل کامل کاربر شامل تمام انتخاب‌ها و پاسخ‌ها
func (h *ProfileHandler) Me(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
payload := fullProfilePayload(user, h.optionRepo, h.questionRepo, h.onboardingService, h.photoRepo)
utils.Success(c, http.StatusOK, "اطلاعات پروفایل با موفقیت دریافت شد", payload)
}
// UploadPhoto POST /profile/photos (multipart/form-data, فیلد "photo")
func (h *ProfileHandler) UploadPhoto(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
fileHeader, err := c.FormFile("photo")
if err != nil {
utils.Fail(c, http.StatusBadRequest, "فایل عکس ارسال نشده", nil)
return
}
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
file, err := fileHeader.Open()
if err != nil {
utils.Fail(c, http.StatusBadRequest, "خطا در خواندن فایل", nil)
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "خطا در خواندن فایل", nil)
return
}
photo, err := h.profileService.UploadPhoto(user.ID, ext, data)
if err != nil {
h.handlePhotoError(c, err)
return
}
utils.Success(c, http.StatusCreated, "عکس اضافه شد", photo)
}
// DeletePhoto DELETE /profile/photos/:id
func (h *ProfileHandler) DeletePhoto(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
if err := h.profileService.DeletePhoto(user.ID, uint(id)); err != nil {
h.handlePhotoError(c, err)
return
}
utils.Success(c, http.StatusOK, "عکس حذف شد", nil)
}
func (h *ProfileHandler) handlePhotoError(c *gin.Context, err error) {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
utils.Fail(c, http.StatusNotFound, "عکس یافت نشد", nil)
case errors.Is(err, service.ErrPhotoNotOwned):
utils.Fail(c, http.StatusForbidden, err.Error(), nil)
case errors.Is(err, service.ErrTooManyPhotos),
errors.Is(err, service.ErrInvalidImage),
errors.Is(err, service.ErrImageTooLarge):
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
default:
utils.Fail(c, http.StatusInternalServerError, "خطا در ذخیره‌ی عکس", nil)
}
}
// UpdateLocation PATCH /profile/location
func (h *ProfileHandler) UpdateLocation(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.UpdateLocationRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.profileService.UpdateLocation(user, req.Latitude, req.Longitude); err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در ذخیره‌ی موقعیت مکانی", nil)
return
}
utils.Success(c, http.StatusOK, "موقعیت مکانی ثبت شد", nil)
}
// UpdateBio PATCH /profile/bio body: {bio}
func (h *ProfileHandler) UpdateBio(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.UpdateBioRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "بیوگرافی نباید بیشتر از ۳۰۰ کاراکتر باشه", err.Error())
return
}
if err := h.profileService.UpdateBio(user, req.Bio); err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در ذخیره‌ی بیوگرافی", nil)
return
}
utils.Success(c, http.StatusOK, "بیوگرافی ذخیره شد", nil)
}
// CheckIn POST /profile/checkin — ثبت ورود روزانه برای استریک گیمیفیکیشن
func (h *ProfileHandler) CheckIn(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
result, err := h.profileService.CheckIn(user)
if err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در ثبت ورود روزانه", nil)
return
}
utils.Success(c, http.StatusOK, "", result)
}
// UpdateNotificationSettings PATCH /profile/notifications
func (h *ProfileHandler) UpdateNotificationSettings(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.UpdateNotificationSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
if err := h.profileService.UpdateNotificationSettings(user, req); err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در ذخیره‌ی تنظیمات اعلان", nil)
return
}
utils.Success(c, http.StatusOK, "تنظیمات اعلان ذخیره شد", nil)
}
// DeleteAccount DELETE /profile/me — غیرفعال کردن (soft delete) حساب کاربر
func (h *ProfileHandler) DeleteAccount(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
if err := h.profileService.DeleteAccount(user); err != nil {
utils.Fail(c, http.StatusInternalServerError, "خطا در حذف حساب", nil)
return
}
utils.Success(c, http.StatusOK, "حساب کاربری غیرفعال شد", nil)
}
// UpdatePublicKey PUT /profile/public-key — ثبت/جایگزینی کلید عمومی X25519
// برای رمزنگاری سرتاسری پیام‌ها. کلاینت این کلید رو خودش تولید می‌کنه؛ کلید
// خصوصی متناظرش هیچ‌وقت به این endpoint (یا هیچ endpoint دیگه‌ای) فرستاده نمی‌شه.
func (h *ProfileHandler) UpdatePublicKey(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var req dto.UpdatePublicKeyRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "کلید عمومی نامعتبر است", err.Error())
return
}
if err := h.profileService.UpdatePublicKey(user, req.PublicKey); err != nil {
if errors.Is(err, service.ErrInvalidPublicKey) {
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
return
}
utils.Fail(c, http.StatusInternalServerError, "خطا در ذخیره‌ی کلید عمومی", nil)
return
}
utils.Success(c, http.StatusOK, "کلید عمومی ذخیره شد", nil)
}