package handler import ( "fmt" app_errors "gpt-load/internal/errors" "gpt-load/internal/models" "gpt-load/internal/response" "log" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "gorm.io/gorm" ) // validateGroupIDFromQuery validates and parses group ID from a query parameter. // Returns 0 and false if validation fails (error is already sent to client) func validateGroupIDFromQuery(c *gin.Context) (uint, bool) { groupIDStr := c.Query("group_id") if groupIDStr == "" { response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.group_id_required") return 0, false } groupID, err := strconv.Atoi(groupIDStr) if err != nil || groupID <= 0 { response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id_format") return 0, false } return uint(groupID), true } // validateKeysText validates the keys text input // Returns false if validation fails (error is already sent to client) func validateKeysText(c *gin.Context, keysText string) bool { if strings.TrimSpace(keysText) == "" { response.ErrorI18nFromAPIError(c, app_errors.ErrValidation, "validation.keys_text_empty") return false } return true } // findGroupByID is a helper function to find a group by its ID. func (s *Server) findGroupByID(c *gin.Context, groupID uint) (*models.Group, bool) { var group models.Group if err := s.DB.First(&group, groupID).Error; err != nil { if err == gorm.ErrRecordNotFound { response.Error(c, app_errors.ErrResourceNotFound) } else { response.Error(c, app_errors.ParseDBError(err)) } return nil, false } return &group, true } // KeyTextRequest defines a generic payload for operations requiring a group ID and a text block of keys. type KeyTextRequest struct { GroupID uint `json:"group_id" binding:"required"` KeysText string `json:"keys_text" binding:"required"` } // GroupIDRequest defines a generic payload for operations requiring only a group ID. type GroupIDRequest struct { GroupID uint `json:"group_id" binding:"required"` } // ValidateGroupKeysRequest defines the payload for validating keys in a group. type ValidateGroupKeysRequest struct { GroupID uint `json:"group_id" binding:"required"` Status string `json:"status,omitempty"` } // AddMultipleKeys handles creating new keys from a text block within a specific group. func (s *Server) AddMultipleKeys(c *gin.Context) { var req KeyTextRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } if _, ok := s.findGroupByID(c, req.GroupID); !ok { return } if !validateKeysText(c, req.KeysText) { return } result, err := s.KeyService.AddMultipleKeys(req.GroupID, req.KeysText) if err != nil { if strings.Contains(err.Error(), "batch size exceeds the limit") { response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error())) } else if err.Error() == "no valid keys found in the input text" { response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error())) } else { response.Error(c, app_errors.ParseDBError(err)) } return } response.Success(c, result) } // AddMultipleKeysAsync handles creating new keys from a text block within a specific group. func (s *Server) AddMultipleKeysAsync(c *gin.Context) { var req KeyTextRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } group, ok := s.findGroupByID(c, req.GroupID) if !ok { return } if !validateKeysText(c, req.KeysText) { return } taskStatus, err := s.KeyImportService.StartImportTask(group, req.KeysText) if err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrTaskInProgress, err.Error())) return } response.Success(c, taskStatus) } // ListKeysInGroup handles listing all keys within a specific group with pagination. func (s *Server) ListKeysInGroup(c *gin.Context) { groupID, ok := validateGroupIDFromQuery(c) if !ok { return } if _, ok := s.findGroupByID(c, groupID); !ok { return } statusFilter := c.Query("status") if statusFilter != "" && statusFilter != models.KeyStatusActive && statusFilter != models.KeyStatusInvalid { response.ErrorI18nFromAPIError(c, app_errors.ErrValidation, "validation.invalid_status_filter") return } searchKeyword := c.Query("key_value") searchHash := "" if searchKeyword != "" { searchHash = s.EncryptionSvc.Hash(searchKeyword) } query := s.KeyService.ListKeysInGroupQuery(groupID, statusFilter, searchHash) var keys []models.APIKey paginatedResult, err := response.Paginate(c, query, &keys) if err != nil { response.Error(c, app_errors.ParseDBError(err)) return } // Decrypt all keys for display for i := range keys { decryptedValue, err := s.EncryptionSvc.Decrypt(keys[i].KeyValue) if err != nil { logrus.WithError(err).WithField("key_id", keys[i].ID).Error("Failed to decrypt key value for listing") keys[i].KeyValue = "failed-to-decrypt" } else { keys[i].KeyValue = decryptedValue } } paginatedResult.Items = keys response.Success(c, paginatedResult) } // DeleteMultipleKeys handles deleting keys from a text block within a specific group. func (s *Server) DeleteMultipleKeys(c *gin.Context) { var req KeyTextRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } if _, ok := s.findGroupByID(c, req.GroupID); !ok { return } if !validateKeysText(c, req.KeysText) { return } result, err := s.KeyService.DeleteMultipleKeys(req.GroupID, req.KeysText) if err != nil { if strings.Contains(err.Error(), "batch size exceeds the limit") { response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error())) } else if err.Error() == "no valid keys found in the input text" { response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error())) } else { response.Error(c, app_errors.ParseDBError(err)) } return } response.Success(c, result) } // DeleteMultipleKeysAsync handles deleting keys from a text block within a specific group using async task. func (s *Server) DeleteMultipleKeysAsync(c *gin.Context) { var req KeyTextRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } group, ok := s.findGroupByID(c, req.GroupID) if !ok { return } if !validateKeysText(c, req.KeysText) { return } taskStatus, err := s.KeyDeleteService.StartDeleteTask(group, req.KeysText) if err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrTaskInProgress, err.Error())) return } response.Success(c, taskStatus) } // RestoreMultipleKeys handles restoring keys from a text block within a specific group. func (s *Server) RestoreMultipleKeys(c *gin.Context) { var req KeyTextRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } if _, ok := s.findGroupByID(c, req.GroupID); !ok { return } if !validateKeysText(c, req.KeysText) { return } result, err := s.KeyService.RestoreMultipleKeys(req.GroupID, req.KeysText) if err != nil { if strings.Contains(err.Error(), "batch size exceeds the limit") { response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error())) } else if err.Error() == "no valid keys found in the input text" { response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error())) } else { response.Error(c, app_errors.ParseDBError(err)) } return } response.Success(c, result) } // TestMultipleKeys handles a one-off validation test for multiple keys. func (s *Server) TestMultipleKeys(c *gin.Context) { var req KeyTextRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } groupDB, ok := s.findGroupByID(c, req.GroupID) if !ok { return } group, err := s.GroupManager.GetGroupByName(groupDB.Name) if err != nil { response.ErrorI18nFromAPIError(c, app_errors.ErrResourceNotFound, "validation.group_not_found") return } if !validateKeysText(c, req.KeysText) { return } start := time.Now() results, err := s.KeyService.TestMultipleKeys(group, req.KeysText) duration := time.Since(start).Milliseconds() if err != nil { if strings.Contains(err.Error(), "batch size exceeds the limit") { response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error())) } else if err.Error() == "no valid keys found in the input text" { response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error())) } else { response.Error(c, app_errors.ParseDBError(err)) } return } response.Success(c, gin.H{ "results": results, "total_duration": duration, }) } // ValidateGroupKeys initiates a manual validation task for all keys in a group. func (s *Server) ValidateGroupKeys(c *gin.Context) { var req ValidateGroupKeysRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } // Validate status if provided if req.Status != "" && req.Status != models.KeyStatusActive && req.Status != models.KeyStatusInvalid { response.ErrorI18nFromAPIError(c, app_errors.ErrValidation, "validation.invalid_status_value") return } groupDB, ok := s.findGroupByID(c, req.GroupID) if !ok { return } group, err := s.GroupManager.GetGroupByName(groupDB.Name) if err != nil { response.ErrorI18nFromAPIError(c, app_errors.ErrResourceNotFound, "validation.group_not_found") return } taskStatus, err := s.KeyManualValidationService.StartValidationTask(group, req.Status) if err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrTaskInProgress, err.Error())) return } response.Success(c, taskStatus) } // RestoreAllInvalidKeys sets the status of all 'inactive' keys in a group to 'active'. func (s *Server) RestoreAllInvalidKeys(c *gin.Context) { var req GroupIDRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } if _, ok := s.findGroupByID(c, req.GroupID); !ok { return } rowsAffected, err := s.KeyService.RestoreAllInvalidKeys(req.GroupID) if err != nil { response.Error(c, app_errors.ParseDBError(err)) return } response.SuccessI18n(c, "success.keys_restored", nil, map[string]any{"count": rowsAffected}) } // ClearAllInvalidKeys deletes all 'inactive' keys from a group. func (s *Server) ClearAllInvalidKeys(c *gin.Context) { var req GroupIDRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } if _, ok := s.findGroupByID(c, req.GroupID); !ok { return } rowsAffected, err := s.KeyService.ClearAllInvalidKeys(req.GroupID) if err != nil { response.Error(c, app_errors.ParseDBError(err)) return } response.SuccessI18n(c, "success.invalid_keys_cleared", nil, map[string]any{"count": rowsAffected}) } // ClearAllKeys deletes all keys from a group. func (s *Server) ClearAllKeys(c *gin.Context) { var req GroupIDRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error())) return } if _, ok := s.findGroupByID(c, req.GroupID); !ok { return } rowsAffected, err := s.KeyService.ClearAllKeys(req.GroupID) if err != nil { response.Error(c, app_errors.ParseDBError(err)) return } response.SuccessI18n(c, "success.all_keys_cleared", nil, map[string]any{"count": rowsAffected}) } // ExportKeys handles exporting keys to a text file. func (s *Server) ExportKeys(c *gin.Context) { groupID, ok := validateGroupIDFromQuery(c) if !ok { return } statusFilter := c.Query("status") if statusFilter == "" { statusFilter = "all" } switch statusFilter { case "all", models.KeyStatusActive, models.KeyStatusInvalid: default: response.ErrorI18nFromAPIError(c, app_errors.ErrValidation, "validation.invalid_status_filter") return } group, ok := s.findGroupByID(c, groupID) if !ok { return } filename := fmt.Sprintf("keys-%s-%s.txt", group.Name, statusFilter) c.Header("Content-Disposition", "attachment; filename="+filename) c.Header("Content-Type", "text/plain; charset=utf-8") if err := s.KeyService.StreamKeysToWriter(groupID, statusFilter, c.Writer); err != nil { log.Printf("Failed to stream keys: %v", err) } }