@@ -4,6 +4,7 @@ package handler
44import (
55 "encoding/json"
66 "fmt"
7+ "net/http"
78 "net/url"
89 "sync"
910
@@ -154,9 +155,25 @@ func (s *Server) validateAndCleanConfig(configMap map[string]any) (map[string]an
154155 return finalMap , nil
155156}
156157
158+ // GroupCreateRequest defines the payload for creating a group.
159+ type GroupCreateRequest struct {
160+ Name string `json:"name"`
161+ DisplayName string `json:"display_name"`
162+ Description string `json:"description"`
163+ Upstreams json.RawMessage `json:"upstreams"`
164+ ChannelType string `json:"channel_type"`
165+ Sort int `json:"sort"`
166+ TestModel string `json:"test_model"`
167+ ValidationEndpoint string `json:"validation_endpoint"`
168+ ParamOverrides map [string ]any `json:"param_overrides"`
169+ Config map [string ]any `json:"config"`
170+ HeaderRules []models.HeaderRule `json:"header_rules"`
171+ ProxyKeys string `json:"proxy_keys"`
172+ }
173+
157174// CreateGroup handles the creation of a new group.
158175func (s * Server ) CreateGroup (c * gin.Context ) {
159- var req models. Group
176+ var req GroupCreateRequest
160177 if err := c .ShouldBindJSON (& req ); err != nil {
161178 response .Error (c , app_errors .NewAPIError (app_errors .ErrInvalidJSON , err .Error ()))
162179 return
@@ -182,7 +199,7 @@ func (s *Server) CreateGroup(c *gin.Context) {
182199 return
183200 }
184201
185- cleanedUpstreams , err := validateAndCleanUpstreams (json . RawMessage ( req .Upstreams ) )
202+ cleanedUpstreams , err := validateAndCleanUpstreams (req .Upstreams )
186203 if err != nil {
187204 response .Error (c , app_errors .NewAPIError (app_errors .ErrValidation , err .Error ()))
188205 return
@@ -200,6 +217,48 @@ func (s *Server) CreateGroup(c *gin.Context) {
200217 return
201218 }
202219
220+ // Validate and normalize header rules if provided
221+ var headerRulesJSON datatypes.JSON
222+ if len (req .HeaderRules ) > 0 {
223+ normalizedHeaderRules := make ([]models.HeaderRule , 0 )
224+ seenKeys := make (map [string ]bool )
225+
226+ for _ , rule := range req .HeaderRules {
227+ key := strings .TrimSpace (rule .Key )
228+ if key == "" {
229+ continue
230+ }
231+
232+ // Normalize to canonical form
233+ canonicalKey := http .CanonicalHeaderKey (key )
234+
235+ // Check for duplicate keys
236+ if seenKeys [canonicalKey ] {
237+ response .Error (c , app_errors .NewAPIError (app_errors .ErrValidation , fmt .Sprintf ("Duplicate header key: %s" , canonicalKey )))
238+ return
239+ }
240+ seenKeys [canonicalKey ] = true
241+
242+ normalizedHeaderRules = append (normalizedHeaderRules , models.HeaderRule {
243+ Key : canonicalKey ,
244+ Value : rule .Value ,
245+ Action : rule .Action ,
246+ })
247+ }
248+
249+ if len (normalizedHeaderRules ) > 0 {
250+ headerRulesBytes , err := json .Marshal (normalizedHeaderRules )
251+ if err != nil {
252+ response .Error (c , app_errors .NewAPIError (app_errors .ErrInternalServer , fmt .Sprintf ("Failed to process header rules: %v" , err )))
253+ return
254+ }
255+ headerRulesJSON = headerRulesBytes
256+ }
257+ }
258+ if headerRulesJSON == nil {
259+ headerRulesJSON = datatypes .JSON ("[]" )
260+ }
261+
203262 group := models.Group {
204263 Name : name ,
205264 DisplayName : strings .TrimSpace (req .DisplayName ),
@@ -211,6 +270,7 @@ func (s *Server) CreateGroup(c *gin.Context) {
211270 ValidationEndpoint : validationEndpoint ,
212271 ParamOverrides : req .ParamOverrides ,
213272 Config : cleanedConfig ,
273+ HeaderRules : headerRulesJSON ,
214274 ProxyKeys : strings .TrimSpace (req .ProxyKeys ),
215275 }
216276
@@ -244,17 +304,18 @@ func (s *Server) ListGroups(c *gin.Context) {
244304// GroupUpdateRequest defines the payload for updating a group.
245305// Using a dedicated struct avoids issues with zero values being ignored by GORM's Update.
246306type GroupUpdateRequest struct {
247- Name * string `json:"name,omitempty"`
248- DisplayName * string `json:"display_name,omitempty"`
249- Description * string `json:"description,omitempty"`
250- Upstreams json.RawMessage `json:"upstreams"`
251- ChannelType * string `json:"channel_type,omitempty"`
252- Sort * int `json:"sort"`
253- TestModel string `json:"test_model"`
254- ValidationEndpoint * string `json:"validation_endpoint,omitempty"`
255- ParamOverrides map [string ]any `json:"param_overrides"`
256- Config map [string ]any `json:"config"`
257- ProxyKeys * string `json:"proxy_keys,omitempty"`
307+ Name * string `json:"name,omitempty"`
308+ DisplayName * string `json:"display_name,omitempty"`
309+ Description * string `json:"description,omitempty"`
310+ Upstreams json.RawMessage `json:"upstreams"`
311+ ChannelType * string `json:"channel_type,omitempty"`
312+ Sort * int `json:"sort"`
313+ TestModel string `json:"test_model"`
314+ ValidationEndpoint * string `json:"validation_endpoint,omitempty"`
315+ ParamOverrides map [string ]any `json:"param_overrides"`
316+ Config map [string ]any `json:"config"`
317+ HeaderRules []models.HeaderRule `json:"header_rules"`
318+ ProxyKeys * string `json:"proxy_keys,omitempty"`
258319}
259320
260321// UpdateGroup handles updating an existing group.
@@ -357,6 +418,48 @@ func (s *Server) UpdateGroup(c *gin.Context) {
357418 group .ProxyKeys = strings .TrimSpace (* req .ProxyKeys )
358419 }
359420
421+ // Handle header rules update
422+ if req .HeaderRules != nil {
423+ var headerRulesJSON datatypes.JSON
424+ normalizedHeaderRules := make ([]models.HeaderRule , 0 )
425+ seenKeys := make (map [string ]bool )
426+
427+ for _ , rule := range req .HeaderRules {
428+ key := strings .TrimSpace (rule .Key )
429+ if key == "" {
430+ continue
431+ }
432+
433+ // Normalize to canonical form
434+ canonicalKey := http .CanonicalHeaderKey (key )
435+
436+ // Check for duplicate keys
437+ if seenKeys [canonicalKey ] {
438+ response .Error (c , app_errors .NewAPIError (app_errors .ErrValidation , fmt .Sprintf ("Duplicate header key: %s" , canonicalKey )))
439+ return
440+ }
441+ seenKeys [canonicalKey ] = true
442+
443+ normalizedHeaderRules = append (normalizedHeaderRules , models.HeaderRule {
444+ Key : canonicalKey ,
445+ Value : rule .Value ,
446+ Action : rule .Action ,
447+ })
448+ }
449+
450+ if len (normalizedHeaderRules ) > 0 {
451+ headerRulesBytes , err := json .Marshal (normalizedHeaderRules )
452+ if err != nil {
453+ response .Error (c , app_errors .NewAPIError (app_errors .ErrInternalServer , fmt .Sprintf ("Failed to process header rules: %v" , err )))
454+ return
455+ }
456+ headerRulesJSON = headerRulesBytes
457+ } else {
458+ headerRulesJSON = datatypes .JSON ("[]" )
459+ }
460+ group .HeaderRules = headerRulesJSON
461+ }
462+
360463 // Save the updated group object
361464 if err := tx .Save (& group ).Error ; err != nil {
362465 response .Error (c , app_errors .ParseDBError (err ))
@@ -376,22 +479,23 @@ func (s *Server) UpdateGroup(c *gin.Context) {
376479
377480// GroupResponse defines the structure for a group response, excluding sensitive or large fields.
378481type GroupResponse struct {
379- ID uint `json:"id"`
380- Name string `json:"name"`
381- Endpoint string `json:"endpoint"`
382- DisplayName string `json:"display_name"`
383- Description string `json:"description"`
384- Upstreams datatypes.JSON `json:"upstreams"`
385- ChannelType string `json:"channel_type"`
386- Sort int `json:"sort"`
387- TestModel string `json:"test_model"`
388- ValidationEndpoint string `json:"validation_endpoint"`
389- ParamOverrides datatypes.JSONMap `json:"param_overrides"`
390- Config datatypes.JSONMap `json:"config"`
391- ProxyKeys string `json:"proxy_keys"`
392- LastValidatedAt * time.Time `json:"last_validated_at"`
393- CreatedAt time.Time `json:"created_at"`
394- UpdatedAt time.Time `json:"updated_at"`
482+ ID uint `json:"id"`
483+ Name string `json:"name"`
484+ Endpoint string `json:"endpoint"`
485+ DisplayName string `json:"display_name"`
486+ Description string `json:"description"`
487+ Upstreams datatypes.JSON `json:"upstreams"`
488+ ChannelType string `json:"channel_type"`
489+ Sort int `json:"sort"`
490+ TestModel string `json:"test_model"`
491+ ValidationEndpoint string `json:"validation_endpoint"`
492+ ParamOverrides datatypes.JSONMap `json:"param_overrides"`
493+ Config datatypes.JSONMap `json:"config"`
494+ HeaderRules []models.HeaderRule `json:"header_rules"`
495+ ProxyKeys string `json:"proxy_keys"`
496+ LastValidatedAt * time.Time `json:"last_validated_at"`
497+ CreatedAt time.Time `json:"created_at"`
498+ UpdatedAt time.Time `json:"updated_at"`
395499}
396500
397501// newGroupResponse creates a new GroupResponse from a models.Group.
@@ -406,6 +510,15 @@ func (s *Server) newGroupResponse(group *models.Group) *GroupResponse {
406510 }
407511 }
408512
513+ // Parse header rules from JSON
514+ var headerRules []models.HeaderRule
515+ if len (group .HeaderRules ) > 0 {
516+ if err := json .Unmarshal (group .HeaderRules , & headerRules ); err != nil {
517+ logrus .WithError (err ).Error ("Failed to unmarshal header rules" )
518+ headerRules = make ([]models.HeaderRule , 0 )
519+ }
520+ }
521+
409522 return & GroupResponse {
410523 ID : group .ID ,
411524 Name : group .Name ,
@@ -419,6 +532,7 @@ func (s *Server) newGroupResponse(group *models.Group) *GroupResponse {
419532 ValidationEndpoint : group .ValidationEndpoint ,
420533 ParamOverrides : group .ParamOverrides ,
421534 Config : group .Config ,
535+ HeaderRules : headerRules ,
422536 ProxyKeys : group .ProxyKeys ,
423537 LastValidatedAt : group .LastValidatedAt ,
424538 CreatedAt : group .CreatedAt ,
0 commit comments