4 Commits

11 changed files with 351 additions and 63 deletions

View File

@@ -7,6 +7,10 @@ VITE_SERVER_PORT=4000
# lstv2 loc # lstv2 loc
LSTV2="C\drive\loc" LSTV2="C\drive\loc"
# discord - this us used to monitor the logs and make sure we never have a critial shut down.
# this will be for other critical stuff like nice label and some other events to make sure we are still in a good spot and dont need to jump in
WEBHOOK=
# dev stuff below # dev stuff below
# Gitea Info # Gitea Info

View File

@@ -3,6 +3,7 @@ module lst.net
go 1.24.3 go 1.24.3
require ( require (
github.com/bensch777/discord-webhook-golang v0.0.6
github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1 github.com/gin-gonic/gin v1.10.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0

View File

@@ -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()
}
}

View File

@@ -8,12 +8,14 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"lst.net/internal/notifications/ws" "lst.net/internal/notifications/ws"
"lst.net/internal/router/middleware"
"lst.net/internal/system/servers" "lst.net/internal/system/servers"
"lst.net/internal/system/settings" "lst.net/internal/system/settings"
"lst.net/pkg/logger" "lst.net/pkg/logger"
) )
func Setup(db *gorm.DB, basePath string, log *logger.CustomLogger) *gin.Engine { func Setup(db *gorm.DB, basePath string, log *logger.CustomLogger) *gin.Engine {
r := gin.Default() r := gin.Default()
if os.Getenv("APP_ENV") == "production" { if os.Getenv("APP_ENV") == "production" {
@@ -34,8 +36,12 @@ func Setup(db *gorm.DB, basePath string, log *logger.CustomLogger) *gin.Engine {
r.StaticFS(basePath+"/docs", http.Dir("docs")) r.StaticFS(basePath+"/docs", http.Dir("docs"))
r.StaticFS(basePath+"/app", http.Dir("frontend")) r.StaticFS(basePath+"/app", http.Dir("frontend"))
r.GET(basePath+"/api/ping", func(c *gin.Context) { // all routes to there respective systems.
log := logger.New() ws.RegisterSocketRoutes(r, basePath, log, db)
settings.RegisterSettingsRoutes(r, basePath, log, db)
servers.RegisterServersRoutes(r, basePath, log, db)
r.GET(basePath+"/api/ping", middleware.SettingCheckMiddleware("testingApiFunction"), func(c *gin.Context) {
log.Info("Checking if the server is up", "system", map[string]interface{}{ log.Info("Checking if the server is up", "system", map[string]interface{}{
"endpoint": "/api/ping", "endpoint": "/api/ping",
"client_ip": c.ClientIP(), "client_ip": c.ClientIP(),
@@ -44,22 +50,17 @@ func Setup(db *gorm.DB, basePath string, log *logger.CustomLogger) *gin.Engine {
c.JSON(200, gin.H{"message": "pong"}) c.JSON(200, gin.H{"message": "pong"})
}) })
// all routes to there respective systems. r.Any(basePath+"/", func(c *gin.Context) { errorApiLoc(c, log) })
ws.RegisterSocketRoutes(r, basePath, log, db)
settings.RegisterSettingsRoutes(r, basePath, log, db)
servers.RegisterServersRoutes(r, basePath, log, db)
r.Any(basePath+"/api", errorApiLoc)
return r return r
} }
func errorApiLoc(c *gin.Context) { func errorApiLoc(c *gin.Context, log *logger.CustomLogger) {
log := logger.New()
log.Error("Api endpoint hit that dose not exist", "system", map[string]interface{}{ log.Error("Api endpoint hit that dose not exist", "system", map[string]interface{}{
"endpoint": "/api", "endpoint": c.Request.URL.Path,
"client_ip": c.ClientIP(), "client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(), "user_agent": c.Request.UserAgent(),
}) })
c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered an api route that dose not exist"}) c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered a route that dose not exist"})
} }

View File

@@ -1,41 +1,39 @@
package settings package settings
import ( import (
"reflect"
"strings"
"gorm.io/gorm" "gorm.io/gorm"
"lst.net/internal/models"
) )
func GetAllSettings(db *gorm.DB) ([]map[string]interface{}, error) { func GetAllSettings(db *gorm.DB) ([]map[string]interface{}, error) {
var settings []models.Settings // var settings []models.Settings
result := db.Find(&settings) // result := db.Find(&settings)
if result.Error != nil { // if result.Error != nil {
return nil, result.Error // return nil, result.Error
} // }
// Function to convert struct to map with lowercase keys // // Function to convert struct to map with lowercase keys
toLowercase := func(s models.Settings) map[string]interface{} { // toLowercase := func(s models.Settings) map[string]interface{} {
t := reflect.TypeOf(s) // t := reflect.TypeOf(s)
v := reflect.ValueOf(s) // v := reflect.ValueOf(s)
data := make(map[string]interface{}) // data := make(map[string]interface{})
for i := 0; i < t.NumField(); i++ { // for i := 0; i < t.NumField(); i++ {
field := strings.ToLower(t.Field(i).Name) // field := strings.ToLower(t.Field(i).Name)
data[field] = v.Field(i).Interface() // data[field] = v.Field(i).Interface()
} // }
return data // return data
} // }
// Convert each struct in settings slice to a map with lowercase keys // // Convert each struct in settings slice to a map with lowercase keys
var lowercaseSettings []map[string]interface{} // var lowercaseSettings []map[string]interface{}
for _, setting := range settings { // for _, setting := range settings {
lowercaseSettings = append(lowercaseSettings, toLowercase(setting)) // lowercaseSettings = append(lowercaseSettings, toLowercase(setting))
} // }
return lowercaseSettings, nil convertedSettings := GetMap()
return convertedSettings, nil
} }

View File

@@ -1,6 +1,9 @@
package settings package settings
import ( import (
"errors"
"fmt"
"gorm.io/gorm" "gorm.io/gorm"
"lst.net/internal/models" "lst.net/internal/models"
"lst.net/pkg/logger" "lst.net/pkg/logger"
@@ -41,31 +44,57 @@ var seedConfigData = []models.Settings{
{Name: "scannerID", Value: `500`, Description: "What scanner id will we be using for the app", AppService: "logistics", Enabled: true}, {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: "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: "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 { func SeedSettings(db *gorm.DB, log *logger.CustomLogger) error {
for _, cfg := range seedConfigData { for _, cfg := range seedConfigData {
var existing models.Settings var existing models.Settings
// Try to find config by unique Name if err := db.Unscoped().Where("name = ?", cfg.Name).First(&existing).Error; err == nil {
result := db.Where("Name =?", cfg.Name).First(&existing)
if result.Error != nil { if existing.DeletedAt.Valid {
if result.Error == gorm.ErrRecordNotFound { // Undelete by setting DeletedAt to NULL
// not here lets add it 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 { if err := db.Create(&cfg).Error; err != nil {
log.Error("Failed to seed settings", "settings", map[string]interface{}{ log.Error("Failed to seed settings", "settings", map[string]interface{}{
"name": cfg.Name, "name": cfg.Name,
"error": err, "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 { } else {
// only update the fields we want to update. // 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.Description = cfg.Description
existing.Name = cfg.Name existing.Name = cfg.Name
existing.AppService = cfg.AppService existing.AppService = cfg.AppService
@@ -76,6 +105,19 @@ func SeedSettings(db *gorm.DB, log *logger.CustomLogger) error {
}) })
return 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) //log.Printf("Updated existing config: %s", cfg.Name)
} }
} }

View File

@@ -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,
})
}

View File

@@ -40,8 +40,16 @@ func UpdateSetting(log *logger.CustomLogger, db *gorm.DB, id string, input Setti
return settingUpdate.Error 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{}{ log.Info("The setting was just updated", "settings", map[string]interface{}{
"name": settingUpdate.Name, "id": id,
"name": cfg.Name,
"updated": updates,
}) })
return nil return nil

View File

@@ -9,6 +9,7 @@ import (
"lst.net/internal/db" "lst.net/internal/db"
"lst.net/internal/router" "lst.net/internal/router"
"lst.net/internal/system/settings"
"lst.net/pkg/logger" "lst.net/pkg/logger"
) )
@@ -25,9 +26,9 @@ func main() {
// Initialize DB // Initialize DB
if _, err := db.InitDB(); err != nil { if _, err := db.InitDB(); err != nil {
log.Panic("Database intialize failed", "db", map[string]interface{}{ log.Panic("Database intialize failed, please check the server asap.", "db", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"casue": errors.Unwrap(err), "cause": errors.Unwrap(err),
"timeout": "30s", "timeout": "30s",
"details": fmt.Sprintf("%+v", err), // Full stack trace if available "details": fmt.Sprintf("%+v", err), // Full stack trace if available
}) })
@@ -43,6 +44,12 @@ func main() {
} }
}() }()
if err := settings.Initialize(db.DB); err != nil {
log.Panic("There was an error intilizing the settings", "settings", map[string]interface{}{
"error": err,
})
}
// long lived process like ocp running all the time should go here and base the db struct over. // long lived process like ocp running all the time should go here and base the db struct over.
// go ocp.MonitorPrinters // go ocp.MonitorPrinters
// go notifcations.Processor // go notifcations.Processor
@@ -54,8 +61,7 @@ func main() {
basePath = "/lst" // Dev only basePath = "/lst" // Dev only
} }
fmt.Println("Welcome to lst backend where all the fun happens.") log.Info("Welcome to lst backend where all the fun happens.", "system", map[string]interface{}{})
// Init Gin router and pass DB to services // Init Gin router and pass DB to services
r := router.Setup(db.DB, basePath, log) r := router.Setup(db.DB, basePath, log)
@@ -66,7 +72,6 @@ func main() {
} }
if err := r.Run(":" + port); err != nil { if err := r.Run(":" + port); err != nil {
log := logger.New()
log.Panic("Server failed to start", "system", map[string]interface{}{ log.Panic("Server failed to start", "system", map[string]interface{}{
"error": err, "error": err,
}) })

View File

@@ -0,0 +1,77 @@
package logger
import (
"encoding/json"
"log"
"os"
"time"
discordwebhook "github.com/bensch777/discord-webhook-golang"
)
func CreateDiscordMsg(message string) {
// we will only run the discord bot if we actaully put a url in the.
if os.Getenv("WEBHOOK") != "" {
var webhookurl = os.Getenv("WEBHOOK")
host, _ := os.Hostname()
embed := discordwebhook.Embed{
Title: "A new crash report from lst.",
Color: 15277667,
Url: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
Timestamp: time.Now(),
// Thumbnail: discordwebhook.Thumbnail{
// Url: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
// },
// Author: discordwebhook.Author{
// Name: "Author Name",
// Icon_URL: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
// },
Fields: []discordwebhook.Field{
discordwebhook.Field{
Name: host,
Value: message,
Inline: false,
},
// discordwebhook.Field{
// Name: "Error reason",
// Value: stack,
// Inline: false,
// },
// discordwebhook.Field{
// Name: "Field 3",
// Value: "Field Value 3",
// Inline: false,
// },
},
// Footer: discordwebhook.Footer{
// Text: "Footer Text",
// Icon_url: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
// },
}
SendEmbed(webhookurl, embed)
} else {
return
}
}
func SendEmbed(link string, embeds discordwebhook.Embed) error {
logging := New()
logging.Info("new messege being posted to discord", "logger", map[string]interface{}{
"message": "Message",
})
hook := discordwebhook.Hook{
Username: "Captain Hook",
Avatar_url: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
Content: "Message",
Embeds: []discordwebhook.Embed{embeds},
}
payload, err := json.Marshal(hook)
if err != nil {
log.Fatal(err)
}
err = discordwebhook.ExecuteWebhook(link, payload)
return err
}

View File

@@ -107,6 +107,7 @@ func (l *CustomLogger) Panic(message, service string, fields map[string]interfac
l.consoleLogger.Warn().Msg("Additional panic context captured") l.consoleLogger.Warn().Msg("Additional panic context captured")
} }
CreateDiscordMsg(message)
panic(message) panic(message)
} }