package controllers import ( "net/http" "strconv" "time" database "goGin/app/database/config" "goGin/app/database/models" "goGin/app/middlewares" utils "goGin/pkg/utis" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // UserResponse, kullanıcı verilerini güvenli bir şekilde döndürmek için type UserResponse struct { ID uint `json:"id"` UserName string `json:"username"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` IsAdmin bool `json:"is_admin"` } // AdminUserListItem, admin listesinde deleted_at ile ayırt etmek için type AdminUserListItem struct { UserResponse DeletedAt *time.Time `json:"deleted_at,omitempty"` } // UserPayload, kullanıcı güncelleme payload'u type UserPayload struct { UserName string `json:"username"` Email string `json:"email"` Password string `json:"password,omitempty"` // Opsiyonel şifre güncellemesi } // AdminUserUpdatePayload, admin tarafından kullanıcı güncelleme type AdminUserUpdatePayload struct { UserName string `json:"username"` Email string `json:"email"` IsAdmin *bool `json:"is_admin"` // Pointer allows checking if field is present } // Helper to convert model to response func toUserResponse(u models.User) UserResponse { isAdmin := false if u.IsAdmin != nil { isAdmin = *u.IsAdmin } isVerified := false if u.EmailVerified != nil { isVerified = *u.EmailVerified } return UserResponse{ ID: u.ID, UserName: u.UserName, Email: u.Email, EmailVerified: isVerified, IsAdmin: isAdmin, } } // toAdminUserListItem, admin listesinde deleted_at döndürmek için func toAdminUserListItem(u models.User) AdminUserListItem { item := AdminUserListItem{UserResponse: toUserResponse(u)} if u.DeletedAt.Valid { item.DeletedAt = &u.DeletedAt.Time } return item } // GetProfile godoc // @Summary Get current user profile // @Description Get profile of the logged-in user // @Tags users // @Security BearerAuth // @Produce json // @Success 200 {object} controllers.UserResponse // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/users/profile [get] func GetProfile(c *gin.Context) { claims, ok := middlewares.GetAuthClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } var user models.User if err := database.DB.Preload("SocialAccounts").Preload("Profile").First(&user, claims.UserID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)}) } // UpdateProfile godoc // @Summary Update current user profile // @Description Update profile of the logged-in user // @Tags users // @Security BearerAuth // @Accept json // @Produce json // @Param user body UserPayload true "User update payload" // @Success 200 {object} controllers.UserResponse // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/users/profile [put] func UpdateProfile(c *gin.Context) { claims, ok := middlewares.GetAuthClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } var payload UserPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var user models.User if err := database.DB.First(&user, claims.UserID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if payload.UserName != "" { user.UserName = payload.UserName } if payload.Email != "" { user.Email = payload.Email // Email değişirse doğrulama sıfırlanabilir f := false user.EmailVerified = &f } if payload.Password != "" { hashed, err := utils.HashPassword(payload.Password) if err == nil { user.Password = hashed } } if err := database.DB.Save(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)}) } // AdminListUsers godoc // @Summary Admin: List users // @Description Admin listing of users with pagination and search // @Tags users_admin // @Security BearerAuth // @Produce json // @Param page query int false "Page number" // @Param per_page query int false "Items per page" // @Param q query string false "Search query (username or email)" // @Param soft query string false "Soft delete filter: only|with" // @Success 200 {object} map[string]interface{} // @Failure 500 {object} map[string]string // @Router /api/v1/admin/users [get] func AdminListUsers(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } pageStr := c.DefaultQuery("page", "1") perPageStr := c.DefaultQuery("per_page", "20") page, _ := strconv.Atoi(pageStr) perPage, _ := strconv.Atoi(perPageStr) if page < 1 { page = 1 } if perPage < 1 { perPage = 20 } if perPage > 100 { perPage = 100 } offset := (page - 1) * perPage soft := c.Query("soft") var query *gorm.DB if soft == "only" { query = database.DB.Unscoped().Model(&models.User{}).Where("deleted_at IS NOT NULL") } else if soft == "with" { query = database.DB.Unscoped().Model(&models.User{}) } else { query = database.DB.Model(&models.User{}) } if q := c.Query("q"); q != "" { like := "%" + q + "%" query = query.Where("user_name LIKE ? OR email LIKE ?", like, like) } var total int64 if err := query.Count(&total).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var users []models.User if err := query.Order("created_at desc").Limit(perPage).Offset(offset).Find(&users).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var data []AdminUserListItem for _, u := range users { data = append(data, toAdminUserListItem(u)) } c.JSON(http.StatusOK, gin.H{"items": data, "total": total, "page": page, "per_page": perPage}) } // AdminGetUser godoc // @Summary Admin: Get user // @Description Get user details by ID // @Tags users_admin // @Security BearerAuth // @Produce json // @Param id path int true "User ID" // @Success 200 {object} controllers.UserResponse // @Failure 404 {object} map[string]string // @Router /api/v1/admin/users/{id} [get] func AdminGetUser(c *gin.Context) { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil || id < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var user models.User // Admin deleted kullanıcıyı da görebilmeli mi? Genelde evet, soft=with ile listede görüyorsa detayda da görmeli. // Varsayılan olarak normal get soft-deleted getirmez. Unscoped kullanalım veya id ile direk bakalım. if err := database.DB.Unscoped().First(&user, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)}) } // AdminUpdateUser godoc // @Summary Admin: Update user // @Description Update user details (admin) // @Tags users_admin // @Security BearerAuth // @Accept json // @Produce json // @Param id path int true "User ID" // @Param user body AdminUserUpdatePayload true "User update payload" // @Success 200 {object} controllers.UserResponse // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/admin/users/{id} [put] func AdminUpdateUser(c *gin.Context) { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil || id < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var payload AdminUserUpdatePayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var user models.User if err := database.DB.Unscoped().First(&user, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if payload.UserName != "" { user.UserName = payload.UserName } if payload.Email != "" { user.Email = payload.Email } if payload.IsAdmin != nil { user.IsAdmin = payload.IsAdmin } if err := database.DB.Save(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)}) } // AdminDeleteUser godoc // @Summary Admin: Delete user // @Description Soft delete user // @Tags users_admin // @Security BearerAuth // @Produce json // @Param id path int true "User ID" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/admin/users/{id} [delete] func AdminDeleteUser(c *gin.Context) { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil || id < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var user models.User if err := database.DB.First(&user, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if err := database.DB.Delete(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "message": "user deleted successfully", "id": user.ID, }) } // AdminRestoreUser godoc // @Summary Admin: Restore user // @Description Restore soft-deleted user // @Tags users_admin // @Security BearerAuth // @Produce json // @Param id path int true "User ID" // @Success 200 {object} controllers.UserResponse // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/admin/users/{id}/restore [post] func AdminRestoreUser(c *gin.Context) { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil || id < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var user models.User if err := database.DB.Unscoped().First(&user, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if user.DeletedAt.Valid { // Restore if err := database.DB.Unscoped().Model(&user).Update("deleted_at", nil).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)}) }