2 Commits

Author SHA1 Message Date
a0aa75c5a0 refactor(settings): changed config to settings and added in the update method for this as well
strict fields on the updates so we can only change what we want in here
2025-07-30 19:35:13 -05:00
78be07c8bb ci(hotreload): added in air for hot reloading 2025-07-30 19:31:02 -05:00
7 changed files with 120 additions and 19 deletions

View File

@@ -10,3 +10,5 @@ this will also include a primary server to house all the common configs across a
The new lst will run in docker by building your own image and deploying or pulling the image down. The new lst will run in docker by building your own image and deploying or pulling the image down.
you will also be able to run it in windows or linux. you will also be able to run it in windows or linux.
when developing in lst and you want to run hotloads installed and configure https://github.com/air-verse/air

0
backend/.air.toml Normal file
View File

View File

@@ -1,25 +1,21 @@
package settings package settings
import ( import (
"encoding/json"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"lst.net/utils/db" "lst.net/utils/db"
"lst.net/utils/inputs"
logging "lst.net/utils/logger" logging "lst.net/utils/logger"
) )
type SettingUpdateInput struct {
Description *string `json:"description"`
Value *string `json:"value"`
Enabled *bool `json:"enabled"`
AppService *string `json:"app_service"`
}
func RegisterSettingsRoutes(l *gin.Engine, baseUrl string) { func RegisterSettingsRoutes(l *gin.Engine, baseUrl string) {
// seed the db on start up // seed the db on start up
db.SeedConfigs(db.DB) db.SeedConfigs(db.DB)
s := l.Group(baseUrl + "/api/v1") s := l.Group(baseUrl + "/api/v1")
s.GET("/settings", getSettings) s.GET("/settings", getSettings)
s.PATCH("/settings", updateSettingById) s.PATCH("/settings/:id", updateSettingById)
} }
func getSettings(c *gin.Context) { func getSettings(c *gin.Context) {
@@ -39,6 +35,7 @@ func getSettings(c *gin.Context) {
"error": err, "error": err,
}) })
c.JSON(500, gin.H{"message": "There was an error getting the settings", "error": err}) c.JSON(500, gin.H{"message": "There was an error getting the settings", "error": err})
return
} }
c.JSON(200, gin.H{"message": "Current settings", "data": configs}) c.JSON(200, gin.H{"message": "Current settings", "data": configs})
@@ -46,18 +43,44 @@ func getSettings(c *gin.Context) {
func updateSettingById(c *gin.Context) { func updateSettingById(c *gin.Context) {
logger := logging.New() logger := logging.New()
var setting SettingUpdateInput settingID := c.Param("id")
err := c.ShouldBindBodyWithJSON(&setting) if settingID == "" {
c.JSON(500, gin.H{"message": "Invalid data"})
logger.Error("Invalid data", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
return
}
var setting inputs.SettingUpdateInput
if err != nil { //err := c.ShouldBindBodyWithJSON(&setting)
c.JSON(500, gin.H{"message": "Internal Server Error"})
logger.Error("Current Settings", "system", map[string]interface{}{ decoder := json.NewDecoder(c.Request.Body) // more strict and will force us to have correct data
decoder.DisallowUnknownFields()
if err := decoder.Decode(&setting); err != nil {
c.JSON(400, gin.H{"message": "Invalid request body", "error": err.Error()})
logger.Error("Invalid request body", "system", map[string]interface{}{
"endpoint": "/api/v1/settings", "endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(), "client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(), "user_agent": c.Request.UserAgent(),
"error": err, "error": err,
}) })
return
}
if err := db.UpdateConfig(db.DB, settingID, setting); err != nil {
c.JSON(500, gin.H{"message": "Failed to update setting", "error": err.Error()})
logger.Error("Failed to update setting", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
"error": err,
})
return
} }
c.JSON(200, gin.H{"message": "Setting was just updated", "data": setting}) c.JSON(200, gin.H{"message": "Setting was just updated", "data": setting})

View File

@@ -8,7 +8,7 @@ require (
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/swaggo/swag v1.16.5 github.com/swaggo/swag v1.16.6
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0 gorm.io/gorm v1.30.0
) )

View File

@@ -27,7 +27,7 @@ import (
"lst.net/cmd/services/system/settings" "lst.net/cmd/services/system/settings"
"lst.net/cmd/services/websocket" "lst.net/cmd/services/websocket"
_ "lst.net/docs" // _ "lst.net/docs"
"lst.net/utils/db" "lst.net/utils/db"
logging "lst.net/utils/logger" logging "lst.net/utils/logger"

View File

@@ -2,14 +2,17 @@ package db
import ( import (
"log" "log"
"reflect"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"lst.net/utils/inputs"
) )
type Settings struct { type Settings struct {
ConfigID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"` SettingID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;not null"` Name string `gorm:"uniqueIndex;not null"`
Description string `gorm:"type:text"` Description string `gorm:"type:text"`
Value string `gorm:"not null"` Value string `gorm:"not null"`
@@ -89,11 +92,76 @@ func SeedConfigs(db *gorm.DB) error {
return nil return nil
} }
func GetAllConfigs(db *gorm.DB) ([]Settings, error) { func GetAllConfigs(db *gorm.DB) ([]map[string]interface{}, error) {
var settings []Settings var settings []Settings
result := db.Find(&settings) result := db.Find(&settings)
return settings, result.Error if result.Error != nil {
return nil, result.Error
}
// Function to convert struct to map with lowercase keys
toLowercase := func(s 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, nil
}
func UpdateConfig(db *gorm.DB, id string, input inputs.SettingUpdateInput) error {
var cfg Settings
if err := db.Where("setting_id =?", id).First(&cfg).Error; err != nil {
return err
}
updates := map[string]interface{}{}
if input.Description != nil {
updates["description"] = *input.Description
}
if input.Value != nil {
updates["value"] = *input.Value
}
if input.Enabled != nil {
updates["enabled"] = *input.Enabled
}
if input.AppService != nil {
updates["app_service"] = *input.AppService
}
if len(updates) == 0 {
return nil // nothing to update
}
return db.Model(&cfg).Updates(updates).Error
}
func DeleteConfig(db *gorm.DB, id uint) error {
// Soft delete by ID
return db.Delete(&Settings{}, id).Error
}
func RestoreConfig(db *gorm.DB, id uint) error {
var cfg Settings
if err := db.Unscoped().First(&cfg, id).Error; err != nil {
return err
}
cfg.DeletedAt = gorm.DeletedAt{}
return db.Unscoped().Save(&cfg).Error
} }

View File

@@ -0,0 +1,8 @@
package inputs
type SettingUpdateInput struct {
Description *string `json:"description"`
Value *string `json:"value"`
Enabled *bool `json:"enabled"`
AppService *string `json:"app_service"`
}