package service import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" "acc-server-manager/local/utl/logging" "context" "encoding/json" "fmt" "github.com/google/uuid" ) const ( presetMaxNameLen = 100 presetMaxDescLen = 500 presetMaxSectionBytes = 64 * 1024 // 64 KB per config section ) type ConfigPresetService struct { repository *repository.ConfigPresetRepository configService *ConfigService activityLog *ActivityLogService } func NewConfigPresetService(repo *repository.ConfigPresetRepository, configService *ConfigService, activityLog *ActivityLogService) *ConfigPresetService { return &ConfigPresetService{ repository: repo, configService: configService, activityLog: activityLog, } } func (s *ConfigPresetService) ListPresets(ctx context.Context, filter *model.ConfigPresetFilter) (*[]model.ConfigPreset, error) { return s.repository.GetAll(ctx, filter) } func (s *ConfigPresetService) GetPreset(ctx context.Context, id uuid.UUID) (*model.ConfigPreset, error) { preset, err := s.repository.GetByID(ctx, id) if err != nil { return nil, err } if preset == nil { return nil, fmt.Errorf("preset not found") } return preset, nil } func (s *ConfigPresetService) CreatePreset(ctx context.Context, req *model.ConfigPresetCreateRequest, actorID, actorUsername string) (*model.ConfigPreset, error) { if req.Name == "" { return nil, fmt.Errorf("preset name is required") } if len(req.Name) > presetMaxNameLen { return nil, fmt.Errorf("preset name exceeds maximum length of %d characters", presetMaxNameLen) } if len(req.Description) > presetMaxDescLen { return nil, fmt.Errorf("preset description exceeds maximum length of %d characters", presetMaxDescLen) } preset := &model.ConfigPreset{ Name: req.Name, Description: req.Description, } if err := marshalSections(req.Configuration, req.AssistRules, req.Event, req.EventRules, req.Settings, preset); err != nil { return nil, err } if err := s.repository.Insert(ctx, preset); err != nil { return nil, err } details := fmt.Sprintf(`{"preset_id":%q,"preset_name":%q}`, preset.ID, preset.Name) go func() { if logErr := s.activityLog.Log(context.Background(), &model.ActivityLog{ UserID: actorID, Username: actorUsername, Action: model.ActionPresetCreate, Details: details, }); logErr != nil { logging.Error("Failed to log preset create: %v", logErr) } }() return preset, nil } func (s *ConfigPresetService) UpdatePreset(ctx context.Context, id uuid.UUID, req *model.ConfigPresetUpdateRequest) (*model.ConfigPreset, error) { preset, err := s.GetPreset(ctx, id) if err != nil { return nil, err } if req.Name != nil { if len(*req.Name) == 0 { return nil, fmt.Errorf("preset name cannot be empty") } if len(*req.Name) > presetMaxNameLen { return nil, fmt.Errorf("preset name exceeds maximum length of %d characters", presetMaxNameLen) } preset.Name = *req.Name } if req.Description != nil { if len(*req.Description) > presetMaxDescLen { return nil, fmt.Errorf("preset description exceeds maximum length of %d characters", presetMaxDescLen) } preset.Description = *req.Description } if err := marshalSections(req.Configuration, req.AssistRules, req.Event, req.EventRules, req.Settings, preset); err != nil { return nil, err } if err := s.repository.Update(ctx, preset); err != nil { return nil, err } return preset, nil } func (s *ConfigPresetService) DeletePreset(ctx context.Context, id uuid.UUID) error { return s.repository.Delete(ctx, id) } func (s *ConfigPresetService) ApplyPreset(ctx context.Context, serverID string, presetID uuid.UUID, actorID, actorUsername string) ([]*model.Config, error) { preset, err := s.GetPreset(ctx, presetID) if err != nil { return nil, err } serverUUID, err := uuid.Parse(serverID) if err != nil { return nil, fmt.Errorf("invalid server ID: %v", err) } type sectionEntry struct { fileName string raw string } sections := []sectionEntry{ {ConfigurationJson, preset.Configuration}, {AssistRulesJson, preset.AssistRules}, {EventJson, preset.Event}, {EventRulesJson, preset.EventRules}, {SettingsJson, preset.Settings}, } var results []*model.Config for _, section := range sections { if section.raw == "" { continue } var body map[string]interface{} if err := json.Unmarshal([]byte(section.raw), &body); err != nil { return nil, fmt.Errorf("failed to unmarshal preset section %s: %v", section.fileName, err) } result, err := s.configService.ApplySection(ctx, serverID, section.fileName, &body, actorID, actorUsername) if err != nil { return nil, fmt.Errorf("failed to apply preset section %s: %v", section.fileName, err) } if result != nil { results = append(results, result) } } details := fmt.Sprintf(`{"preset_id":%q,"preset_name":%q,"sections_applied":%d}`, presetID, preset.Name, len(results)) go func() { if logErr := s.activityLog.Log(context.Background(), &model.ActivityLog{ ServerID: &serverUUID, UserID: actorID, Username: actorUsername, Action: model.ActionPresetApply, Details: details, }); logErr != nil { logging.Error("Failed to log preset apply: %v", logErr) } }() return results, nil } // marshalSections marshals each non-nil config section into the preset's JSON string fields, // enforcing the per-section byte size limit. func marshalSections(configuration, assistRules, event, eventRules, settings *map[string]interface{}, preset *model.ConfigPreset) error { type entry struct { src *map[string]interface{} name string dest *string } entries := []entry{ {configuration, "configuration", &preset.Configuration}, {assistRules, "assistRules", &preset.AssistRules}, {event, "event", &preset.Event}, {eventRules, "eventRules", &preset.EventRules}, {settings, "settings", &preset.Settings}, } for _, e := range entries { if e.src == nil { continue } b, err := json.Marshal(e.src) if err != nil { return fmt.Errorf("failed to marshal %s: %v", e.name, err) } if len(b) > presetMaxSectionBytes { return fmt.Errorf("%s section exceeds maximum size of %d bytes", e.name, presetMaxSectionBytes) } *e.dest = string(b) } return nil }