From c7af1901aac2c0292f2acae39bb419fbaf74c908 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Tue, 5 Aug 2025 12:40:34 -0500 Subject: [PATCH] feat(app state): settings are now global and get updated on the fly --- .../router/middleware/settings_Check.go | 41 +++++++ .../internal/system/settings/get_settings.go | 50 ++++---- .../internal/system/settings/settings_seed.go | 80 ++++++++++--- .../system/settings/settings_states.go | 110 ++++++++++++++++++ .../system/settings/update_setting.go | 10 +- 5 files changed, 245 insertions(+), 46 deletions(-) create mode 100644 backend/internal/router/middleware/settings_Check.go create mode 100644 backend/internal/system/settings/settings_states.go diff --git a/backend/internal/router/middleware/settings_Check.go b/backend/internal/router/middleware/settings_Check.go new file mode 100644 index 0000000..1a76538 --- /dev/null +++ b/backend/internal/router/middleware/settings_Check.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "lst.net/internal/system/settings" +) + +func SettingCheckMiddleware(settingName string) gin.HandlerFunc { + return func(c *gin.Context) { + // Debug: Log the setting name we're checking + //log.Printf("Checking setting '%s' for path: %s", settingName, c.Request.URL.Path) + + // Get the current setting value + value, err := settings.GetString(settingName) + if err != nil { + //log.Printf("Error getting setting '%s': %v", settingName, err) + c.AbortWithStatusJSON(404, gin.H{ + "error": "endpoint not available", + "details": "setting error", + }) + return + } + + // Debug: Log the actual value received + //log.Printf("Setting '%s' value: '%s'", settingName, value) + + // Changed condition to check for "1" (enable) instead of "0" (disable) + if value != "1" { + //log.Printf("Setting '%s' not enabled (value: '%s')", settingName, value) + c.AbortWithStatusJSON(404, gin.H{ + "error": "endpoint not available", + "details": "required feature is disabled", + }) + return + } + + // Debug: Log successful check + //log.Printf("Setting check passed for '%s'", settingName) + c.Next() + } +} diff --git a/backend/internal/system/settings/get_settings.go b/backend/internal/system/settings/get_settings.go index 1e3a6d5..513b421 100644 --- a/backend/internal/system/settings/get_settings.go +++ b/backend/internal/system/settings/get_settings.go @@ -1,41 +1,39 @@ package settings import ( - "reflect" - "strings" - "gorm.io/gorm" - "lst.net/internal/models" ) func GetAllSettings(db *gorm.DB) ([]map[string]interface{}, error) { - var settings []models.Settings - result := db.Find(&settings) + // var settings []models.Settings + // result := db.Find(&settings) - if result.Error != nil { - return nil, result.Error - } + // if result.Error != nil { + // return nil, result.Error + // } - // Function to convert struct to map with lowercase keys - toLowercase := func(s models.Settings) map[string]interface{} { - t := reflect.TypeOf(s) - v := reflect.ValueOf(s) + // // Function to convert struct to map with lowercase keys + // toLowercase := func(s models.Settings) map[string]interface{} { + // t := reflect.TypeOf(s) + // v := reflect.ValueOf(s) - data := make(map[string]interface{}) + // data := make(map[string]interface{}) - for i := 0; i < t.NumField(); i++ { - field := strings.ToLower(t.Field(i).Name) - data[field] = v.Field(i).Interface() - } + // for i := 0; i < t.NumField(); i++ { + // field := strings.ToLower(t.Field(i).Name) + // data[field] = v.Field(i).Interface() + // } - return data - } + // return data + // } - // Convert each struct in settings slice to a map with lowercase keys - var lowercaseSettings []map[string]interface{} - for _, setting := range settings { - lowercaseSettings = append(lowercaseSettings, toLowercase(setting)) - } + // // Convert each struct in settings slice to a map with lowercase keys + // var lowercaseSettings []map[string]interface{} + // for _, setting := range settings { + // lowercaseSettings = append(lowercaseSettings, toLowercase(setting)) + // } - return lowercaseSettings, nil + convertedSettings := GetMap() + + return convertedSettings, nil } diff --git a/backend/internal/system/settings/settings_seed.go b/backend/internal/system/settings/settings_seed.go index ca06ff6..080f277 100644 --- a/backend/internal/system/settings/settings_seed.go +++ b/backend/internal/system/settings/settings_seed.go @@ -1,6 +1,9 @@ package settings import ( + "errors" + "fmt" + "gorm.io/gorm" "lst.net/internal/models" "lst.net/pkg/logger" @@ -41,41 +44,80 @@ var seedConfigData = []models.Settings{ {Name: "scannerID", Value: `500`, Description: "What scanner id will we be using for the app", AppService: "logistics", Enabled: true}, {Name: "scannerPort", Value: `50002`, Description: "What port instance will we be using?", AppService: "logistics", Enabled: true}, {Name: "stagingReturnLocations", Value: `30125,31523`, Description: "What are the staging location IDs we will use to select from. seperated by commas", AppService: "logistics", Enabled: true}, + {Name: "testingApiFunction", Value: `1`, Description: "This is a test to validate if we set to 0 it will actaully not allow the route", AppService: "logistics", Enabled: true}, } func SeedSettings(db *gorm.DB, log *logger.CustomLogger) error { for _, cfg := range seedConfigData { var existing models.Settings - // Try to find config by unique Name - result := db.Where("Name =?", cfg.Name).First(&existing) + if err := db.Unscoped().Where("name = ?", cfg.Name).First(&existing).Error; err == nil { - if result.Error != nil { - if result.Error == gorm.ErrRecordNotFound { - // not here lets add it + if existing.DeletedAt.Valid { + // Undelete by setting DeletedAt to NULL + if err := db.Unscoped().Model(&existing).Update("DeletedAt", gorm.DeletedAt{}).Error; err != nil { + log.Error("Failed to undelete settings", "settings", map[string]interface{}{ + "name": cfg.Name, + "error": err, + }) + return nil + } + } + + if errors.Is(err, gorm.ErrRecordNotFound) { if err := db.Create(&cfg).Error; err != nil { log.Error("Failed to seed settings", "settings", map[string]interface{}{ "name": cfg.Name, "error": err, }) } - //log.Printf("Seeded new config: %s", cfg.Name) - } else { - // Some other error - return result.Error } + + // // Try to find config by unique Name + // result := db.Where("Name =?", cfg.Name).First(&existing) + + // if result.Error != nil { + // if result.Error == gorm.ErrRecordNotFound && cfg.Enabled { + // // not here lets add it + + // if err := db.Create(&cfg).Error; err != nil && !existing.DeletedAt.Valid { + // log.Error("Failed to seed settings", "settings", map[string]interface{}{ + // "name": cfg.Name, + // "error": err, + // }) + // } + + // //log.Printf("Seeded new config: %s", cfg.Name) + // } else { + // // Some other error + // return result.Error + // } } else { - // only update the fields we want to update. - existing.Description = cfg.Description - existing.Name = cfg.Name - existing.AppService = cfg.AppService - if err := db.Save(&existing).Error; err != nil { - log.Error("Failed to update ettings.", "settings", map[string]interface{}{ - "name": cfg.Name, - "error": err, - }) - return err + // remove the setting if we change to false this will help with future proofing our seeder in the event we need to add it back + if cfg.Enabled { + existing.Description = cfg.Description + existing.Name = cfg.Name + existing.AppService = cfg.AppService + if err := db.Save(&existing).Error; err != nil { + log.Error("Failed to update ettings.", "settings", map[string]interface{}{ + "name": cfg.Name, + "error": err, + }) + return err + } + } else { + // we delete the setting so its no longer there + if err := db.Delete(&existing).Error; err != nil { + log.Error("Failed to delete ettings.", "settings", map[string]interface{}{ + "name": cfg.Name, + "error": err, + }) + return err + } + settingDelete := fmt.Sprintf("Updated existing config: %s", cfg.Name) + log.Info(settingDelete, "settings", map[string]interface{}{}) } + //log.Printf("Updated existing config: %s", cfg.Name) } } diff --git a/backend/internal/system/settings/settings_states.go b/backend/internal/system/settings/settings_states.go new file mode 100644 index 0000000..0855f29 --- /dev/null +++ b/backend/internal/system/settings/settings_states.go @@ -0,0 +1,110 @@ +package settings + +import ( + "errors" + "fmt" + "reflect" + "strings" + "sync" + + "gorm.io/gorm" + "lst.net/internal/models" +) + +var ( + // Global state + appSettings []models.Settings + appSettingsLock sync.RWMutex + dbInstance *gorm.DB +) + +// Initialize loads settings into memory at startup +func Initialize(db *gorm.DB) error { + dbInstance = db + return Refresh() +} + +// Refresh reloads settings from DB (call after updates) +func Refresh() error { + appSettingsLock.Lock() + defer appSettingsLock.Unlock() + + var settings []models.Settings + if err := dbInstance.Find(&settings).Error; err != nil { + return err + } + + appSettings = settings + return nil +} + +// GetAll returns a thread-safe copy of settings +func GetAll() []models.Settings { + appSettingsLock.RLock() + defer appSettingsLock.RUnlock() + + // Return copy to prevent external modification + copied := make([]models.Settings, len(appSettings)) + copy(copied, appSettings) + return copied +} + +// GetMap returns settings as []map[string]interface{} +func GetMap() []map[string]interface{} { + return convertToMap(GetAll()) +} + +// convertToMap helper (move your existing conversion logic here) +func convertToMap(settings []models.Settings) []map[string]interface{} { + toLowercase := func(s models.Settings) map[string]interface{} { + t := reflect.TypeOf(s) + v := reflect.ValueOf(s) + + data := make(map[string]interface{}) + + for i := 0; i < t.NumField(); i++ { + field := strings.ToLower(t.Field(i).Name) + data[field] = v.Field(i).Interface() + } + + return data + } + + // Convert each struct in settings slice to a map with lowercase keys + var lowercaseSettings []map[string]interface{} + for _, setting := range settings { + lowercaseSettings = append(lowercaseSettings, toLowercase(setting)) + } + + return lowercaseSettings +} + +func GetString(name string) (string, error) { + appSettingsLock.RLock() + defer appSettingsLock.RUnlock() + + for _, s := range appSettings { + if s.Name == name { // assuming your model has a "Name" field + fmt.Println(s.Value) + return s.Value, nil // assuming your model has a "Value" field + } + } + return "", errors.New("setting not found") +} + +func SetTemp(name, value string) { + appSettingsLock.Lock() + defer appSettingsLock.Unlock() + + for i, s := range appSettings { + if s.Name == name { + appSettings[i].Value = value + return + } + } + // If not found, add new setting + appSettings = append(appSettings, models.Settings{ + Name: name, + Value: value, + }) +} diff --git a/backend/internal/system/settings/update_setting.go b/backend/internal/system/settings/update_setting.go index 117e049..edffb25 100644 --- a/backend/internal/system/settings/update_setting.go +++ b/backend/internal/system/settings/update_setting.go @@ -40,8 +40,16 @@ func UpdateSetting(log *logger.CustomLogger, db *gorm.DB, id string, input Setti return settingUpdate.Error } + if err := Refresh(); err != nil { + log.Error("There was an error refreshing the settings after a setting update", "settings", map[string]interface{}{ + "error": err, + }) + } + log.Info("The setting was just updated", "settings", map[string]interface{}{ - "name": settingUpdate.Name, + "id": id, + "name": cfg.Name, + "updated": updates, }) return nil