diff --git a/cmd/api/main.go b/cmd/api/main.go index 804b279..1cbcf2d 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,8 +1,12 @@ package main import ( + "acc-server-manager/local/utl/cache" "acc-server-manager/local/utl/db" + "acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/server" + "fmt" + "os" "github.com/joho/godotenv" "go.uber.org/dig" @@ -12,7 +16,19 @@ import ( func main() { godotenv.Load() + // Initialize logger + logger, err := logging.Initialize() + if err != nil { + fmt.Printf("Failed to initialize logger: %v\n", err) + os.Exit(1) + } + defer logger.Close() + + // Set up panic recovery + defer logging.RecoverAndLog() + di := dig.New() + cache.Start(di) db.Start(di) server.Start(di) } diff --git a/go.mod b/go.mod index 853e454..b555a89 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,16 @@ module acc-server-manager -go 1.22.3 +go 1.23.0 require ( - github.com/gofiber/fiber/v2 v2.52.5 + github.com/gofiber/fiber/v2 v2.52.8 github.com/gofiber/swagger v1.1.0 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c github.com/swaggo/swag v1.16.3 go.uber.org/dig v1.17.1 + golang.org/x/sync v0.15.0 golang.org/x/text v0.16.0 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.11 @@ -17,7 +18,7 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect @@ -25,18 +26,18 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.17.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/tools v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ca0d9bb..7bc537d 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -12,12 +12,12 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= -github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4= +github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ= github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -26,8 +26,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -39,8 +39,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -67,12 +67,12 @@ go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= diff --git a/local/model/config.go b/local/model/config.go index e468777..b0c9f67 100644 --- a/local/model/config.go +++ b/local/model/config.go @@ -9,6 +9,7 @@ import ( ) type IntString int +type IntBool int // Config tracks configuration modifications type Config struct { @@ -90,11 +91,11 @@ type EventRules struct { DriverStIntStringTimeSec IntString `json:"driverStIntStringTimeSec"` MandatoryPitstopCount IntString `json:"mandatoryPitstopCount"` MaxTotalDrivingTime IntString `json:"maxTotalDrivingTime"` - IsRefuellingAllowedInRace bool `json:"isRefuellingAllowedInRace"` - IsRefuellingTimeFixed bool `json:"isRefuellingTimeFixed"` - IsMandatoryPitstopRefuellingRequired bool `json:"isMandatoryPitstopRefuellingRequired"` - IsMandatoryPitstopTyreChangeRequired bool `json:"isMandatoryPitstopTyreChangeRequired"` - IsMandatoryPitstopSwapDriverRequired bool `json:"isMandatoryPitstopSwapDriverRequired"` + IsRefuellingAllowedInRace IntBool `json:"isRefuellingAllowedInRace"` + IsRefuellingTimeFixed IntBool `json:"isRefuellingTimeFixed"` + IsMandatoryPitstopRefuellingRequired IntBool `json:"isMandatoryPitstopRefuellingRequired"` + IsMandatoryPitstopTyreChangeRequired IntBool `json:"isMandatoryPitstopTyreChangeRequired"` + IsMandatoryPitstopSwapDriverRequired IntBool `json:"isMandatoryPitstopSwapDriverRequired"` TyreSetCount IntString `json:"tyreSetCount"` } @@ -127,6 +128,34 @@ const ( CacheKeySystemConfig = "system_config_%s" // Format with config key ) +func (i *IntBool) UnmarshalJSON(b []byte) error { + var str int + if err := json.Unmarshal(b, &str); err == nil && str <= 1 { + *i = IntBool(str) + return nil + } + + var num bool + if err := json.Unmarshal(b, &num); err == nil { + if num { + *i = IntBool(1) + } else { + *i = IntBool(0) + } + return nil + } + + return fmt.Errorf("invalid IntBool value") +} + +func (i IntBool) ToInt() int { + return int(i) +} + +func (i IntBool) ToBool() bool { + return i == 1 +} + func (i *IntString) UnmarshalJSON(b []byte) error { var str string if err := json.Unmarshal(b, &str); err == nil { @@ -148,7 +177,7 @@ func (i *IntString) UnmarshalJSON(b []byte) error { return nil } - return fmt.Errorf("invalid postQualySeconds value") + return fmt.Errorf("invalid IntString value") } func (i IntString) ToString() string { diff --git a/local/model/steam_credentials.go b/local/model/steam_credentials.go index 95dc420..17985ad 100644 --- a/local/model/steam_credentials.go +++ b/local/model/steam_credentials.go @@ -1,6 +1,7 @@ package model import ( + "acc-server-manager/local/utl/configs" "crypto/aes" "crypto/cipher" "crypto/rand" @@ -84,12 +85,10 @@ func (s *SteamCredentials) Validate() error { return nil } -// GetEncryptionKey returns the encryption key, in a real application this should be stored securely -// and potentially rotated periodically +// GetEncryptionKey returns the encryption key from config. +// The key is loaded from the ENCRYPTION_KEY environment variable. func GetEncryptionKey() []byte { - // This is a placeholder - in production, this should be stored securely - // and potentially fetched from a key management service - return []byte("your-32-byte-encryption-key-here") + return []byte(configs.EncryptionKey) } // EncryptPassword encrypts a password using AES-256 diff --git a/local/repository/lookup.go b/local/repository/lookup.go index 1a7c30b..ef3fae1 100644 --- a/local/repository/lookup.go +++ b/local/repository/lookup.go @@ -2,87 +2,90 @@ package repository import ( "acc-server-manager/local/model" + "acc-server-manager/local/utl/cache" "context" + "time" "gorm.io/gorm" ) +const ( + cacheDuration = 1 * time.Hour + tracksCacheKey = "tracks" + carModelsCacheKey = "carModels" + driverCategoriesCacheKey = "driverCategories" + cupCategoriesCacheKey = "cupCategories" + sessionTypesCacheKey = "sessionTypes" +) + type LookupRepository struct { - db *gorm.DB + db *gorm.DB + cache *cache.InMemoryCache } -func NewLookupRepository(db *gorm.DB) *LookupRepository { +func NewLookupRepository(db *gorm.DB, cache *cache.InMemoryCache) *LookupRepository { return &LookupRepository{ - db: db, + db: db, + cache: cache, } } -// GetTracks -// Gets Tracks rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupRepository) GetTracks(ctx context.Context) *[]model.Track { - db := as.db.WithContext(ctx) - TrackModel := new([]model.Track) - db.Find(&TrackModel) - return TrackModel +func (r *LookupRepository) GetTracks(ctx context.Context) (*[]model.Track, error) { + fetcher := func() (*[]model.Track, error) { + db := r.db.WithContext(ctx) + items := new([]model.Track) + if err := db.Find(items).Error; err != nil { + return nil, err + } + return items, nil + } + return cache.GetOrSet(r.cache, tracksCacheKey, cacheDuration, fetcher) } -// GetCarModels -// Gets CarModels rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupRepository) GetCarModels(ctx context.Context) *[]model.CarModel { - db := as.db.WithContext(ctx) - CarModelModel := new([]model.CarModel) - db.Find(&CarModelModel) - return CarModelModel +func (r *LookupRepository) GetCarModels(ctx context.Context) (*[]model.CarModel, error) { + fetcher := func() (*[]model.CarModel, error) { + db := r.db.WithContext(ctx) + items := new([]model.CarModel) + if err := db.Find(items).Error; err != nil { + return nil, err + } + return items, nil + } + return cache.GetOrSet(r.cache, carModelsCacheKey, cacheDuration, fetcher) } -// GetDriverCategories -// Gets DriverCategories rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupRepository) GetDriverCategories(ctx context.Context) *[]model.DriverCategory { - db := as.db.WithContext(ctx) - DriverCategoryModel := new([]model.DriverCategory) - db.Find(&DriverCategoryModel) - return DriverCategoryModel +func (r *LookupRepository) GetDriverCategories(ctx context.Context) (*[]model.DriverCategory, error) { + fetcher := func() (*[]model.DriverCategory, error) { + db := r.db.WithContext(ctx) + items := new([]model.DriverCategory) + if err := db.Find(items).Error; err != nil { + return nil, err + } + return items, nil + } + return cache.GetOrSet(r.cache, driverCategoriesCacheKey, cacheDuration, fetcher) } -// GetCupCategories -// Gets CupCategories rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupRepository) GetCupCategories(ctx context.Context) *[]model.CupCategory { - db := as.db.WithContext(ctx) - CupCategoryModel := new([]model.CupCategory) - db.Find(&CupCategoryModel) - return CupCategoryModel +func (r *LookupRepository) GetCupCategories(ctx context.Context) (*[]model.CupCategory, error) { + fetcher := func() (*[]model.CupCategory, error) { + db := r.db.WithContext(ctx) + items := new([]model.CupCategory) + if err := db.Find(items).Error; err != nil { + return nil, err + } + return items, nil + } + return cache.GetOrSet(r.cache, cupCategoriesCacheKey, cacheDuration, fetcher) } -// GetSessionTypes -// Gets SessionTypes rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupRepository) GetSessionTypes(ctx context.Context) *[]model.SessionType { - db := as.db.WithContext(ctx) - SessionTypesModel := new([]model.SessionType) - db.Find(&SessionTypesModel) - return SessionTypesModel +func (r *LookupRepository) GetSessionTypes(ctx context.Context) (*[]model.SessionType, error) { + fetcher := func() (*[]model.SessionType, error) { + db := r.db.WithContext(ctx) + items := new([]model.SessionType) + if err := db.Find(items).Error; err != nil { + return nil, err + } + return items, nil + } + return cache.GetOrSet(r.cache, sessionTypesCacheKey, cacheDuration, fetcher) } diff --git a/local/repository/server.go b/local/repository/server.go index fca88cd..21ce52d 100644 --- a/local/repository/server.go +++ b/local/repository/server.go @@ -13,11 +13,76 @@ type ServerRepository struct { } func NewServerRepository(db *gorm.DB) *ServerRepository { - return &ServerRepository{ + repo := &ServerRepository{ BaseRepository: NewBaseRepository[model.Server, model.ServerFilter](db, model.Server{}), } + + // Run migrations + if err := repo.migrateServerTable(); err != nil { + panic(err) + } + + return repo } +// migrateServerTable ensures all required columns exist with proper defaults +func (r *ServerRepository) migrateServerTable() error { + // Create a temporary table with all required columns + if err := r.db.Exec(` + CREATE TABLE IF NOT EXISTS servers_new ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + ip TEXT NOT NULL, + port INTEGER NOT NULL DEFAULT 9600, + path TEXT NOT NULL, + service_name TEXT NOT NULL, + date_created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + from_steam_cmd BOOLEAN NOT NULL DEFAULT 1 + ) + `).Error; err != nil { + return err + } + + // Copy data from old table, setting defaults for new columns + if err := r.db.Exec(` + INSERT INTO servers_new ( + id, + name, + ip, + port, + path, + service_name, + date_created, + from_steam_cmd + ) + SELECT + id, + COALESCE(name, 'Server ' || id) as name, + COALESCE(ip, '127.0.0.1') as ip, + COALESCE(port, 9600) as port, + path, + COALESCE(service_name, 'ACC-Server-' || id) as service_name, + COALESCE(date_created, CURRENT_TIMESTAMP) as date_created, + COALESCE(from_steam_cmd, 1) as from_steam_cmd + FROM servers + `).Error; err != nil { + // If the old table doesn't exist, this is a fresh install + if err := r.db.Exec(`DROP TABLE IF EXISTS servers_new`).Error; err != nil { + return err + } + return nil + } + + // Replace old table with new one + if err := r.db.Exec(`DROP TABLE IF EXISTS servers`).Error; err != nil { + return err + } + if err := r.db.Exec(`ALTER TABLE servers_new RENAME TO servers`).Error; err != nil { + return err + } + + return nil +} // GetFirstByServiceName // Gets first row from Server table. diff --git a/local/repository/state_history.go b/local/repository/state_history.go index 13e7095..a9f0f7d 100644 --- a/local/repository/state_history.go +++ b/local/repository/state_history.go @@ -44,3 +44,116 @@ func (r *StateHistoryRepository) GetLastSessionID(ctx context.Context, serverID return lastSession.SessionID, nil } + +// GetSummaryStats calculates peak players, total sessions, and average players. +func (r *StateHistoryRepository) GetSummaryStats(ctx context.Context, filter *model.StateHistoryFilter) (model.StateHistoryStats, error) { + var stats model.StateHistoryStats + query := r.db.WithContext(ctx).Model(&model.StateHistory{}). + Select(` + COALESCE(MAX(player_count), 0) as peak_players, + COUNT(DISTINCT session_id) as total_sessions, + COALESCE(AVG(player_count), 0) as average_players + `). + Where("server_id = ?", filter.ServerID) + + if !filter.StartDate.IsZero() && !filter.EndDate.IsZero() { + query = query.Where("date_created BETWEEN ? AND ?", filter.StartDate, filter.EndDate) + } + + if err := query.Scan(&stats).Error; err != nil { + return model.StateHistoryStats{}, err + } + return stats, nil +} + +// GetTotalPlaytime calculates the total playtime in minutes. +func (r *StateHistoryRepository) GetTotalPlaytime(ctx context.Context, filter *model.StateHistoryFilter) (int, error) { + var totalPlaytime struct { + TotalMinutes float64 + } + rawQuery := ` + SELECT SUM(duration_minutes) as total_minutes FROM ( + SELECT (strftime('%s', MAX(date_created)) - strftime('%s', MIN(date_created))) / 60.0 as duration_minutes + FROM state_histories + WHERE server_id = ? AND date_created BETWEEN ? AND ? + GROUP BY session_id + HAVING COUNT(*) > 1 AND MAX(player_count) > 0 + ) + ` + err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&totalPlaytime).Error + if err != nil { + return 0, err + } + return int(totalPlaytime.TotalMinutes), nil +} + +// GetPlayerCountOverTime gets downsampled player count data. +func (r *StateHistoryRepository) GetPlayerCountOverTime(ctx context.Context, filter *model.StateHistoryFilter) ([]model.PlayerCountPoint, error) { + var points []model.PlayerCountPoint + rawQuery := ` + SELECT + strftime('%Y-%m-%d %H:00:00', date_created) as timestamp, + AVG(player_count) as count + FROM state_histories + WHERE server_id = ? AND date_created BETWEEN ? AND ? + GROUP BY 1 + ORDER BY 1 + ` + err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&points).Error + return points, err +} + +// GetSessionTypes counts sessions by type. +func (r *StateHistoryRepository) GetSessionTypes(ctx context.Context, filter *model.StateHistoryFilter) ([]model.SessionCount, error) { + var sessionTypes []model.SessionCount + rawQuery := ` + SELECT session as name, COUNT(*) as count FROM ( + SELECT session + FROM state_histories + WHERE server_id = ? AND date_created BETWEEN ? AND ? + GROUP BY session_id + ) + GROUP BY session + ORDER BY count DESC + ` + err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&sessionTypes).Error + return sessionTypes, err +} + +// GetDailyActivity counts sessions per day. +func (r *StateHistoryRepository) GetDailyActivity(ctx context.Context, filter *model.StateHistoryFilter) ([]model.DailyActivity, error) { + var dailyActivity []model.DailyActivity + rawQuery := ` + SELECT + DATE(date_created) as date, + COUNT(DISTINCT session_id) as sessions_count + FROM state_histories + WHERE server_id = ? AND date_created BETWEEN ? AND ? + GROUP BY 1 + ORDER BY 1 + ` + err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&dailyActivity).Error + return dailyActivity, err +} + +// GetRecentSessions retrieves the 10 most recent sessions. +func (r *StateHistoryRepository) GetRecentSessions(ctx context.Context, filter *model.StateHistoryFilter) ([]model.RecentSession, error) { + var recentSessions []model.RecentSession + rawQuery := ` + SELECT + session_id as id, + MIN(date_created) as date, + session as type, + track, + MAX(player_count) as players, + CAST((strftime('%s', MAX(date_created)) - strftime('%s', MIN(date_created))) / 60 AS INTEGER) as duration + FROM state_histories + WHERE server_id = ? AND date_created BETWEEN ? AND ? + GROUP BY session_id + HAVING COUNT(*) > 1 AND MAX(player_count) > 0 + ORDER BY date DESC + LIMIT 10 + ` + err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&recentSessions).Error + return recentSessions, err +} diff --git a/local/service/lookup.go b/local/service/lookup.go index fa453cd..1052c3f 100644 --- a/local/service/lookup.go +++ b/local/service/lookup.go @@ -1,7 +1,6 @@ package service import ( - "acc-server-manager/local/model" "acc-server-manager/local/repository" "acc-server-manager/local/utl/logging" @@ -10,85 +9,36 @@ import ( type LookupService struct { repository *repository.LookupRepository - cache *model.LookupCache } -func NewLookupService(repository *repository.LookupRepository, cache *model.LookupCache) *LookupService { +func NewLookupService(repository *repository.LookupRepository) *LookupService { logging.Debug("Initializing LookupService") return &LookupService{ repository: repository, - cache: cache, } } func (s *LookupService) GetTracks(ctx *fiber.Ctx) (interface{}, error) { - if cached, exists := s.cache.Get("tracks"); exists { - return cached, nil - } - - logging.Debug("Loading tracks from database") - tracks := s.repository.GetTracks(ctx.UserContext()) - s.cache.Set("tracks", tracks) - return tracks, nil + logging.Debug("Getting tracks") + return s.repository.GetTracks(ctx.UserContext()) } func (s *LookupService) GetCarModels(ctx *fiber.Ctx) (interface{}, error) { - if cached, exists := s.cache.Get("cars"); exists { - return cached, nil - } - - logging.Debug("Loading car models from database") - cars := s.repository.GetCarModels(ctx.UserContext()) - s.cache.Set("cars", cars) - return cars, nil + logging.Debug("Getting car models") + return s.repository.GetCarModels(ctx.UserContext()) } func (s *LookupService) GetDriverCategories(ctx *fiber.Ctx) (interface{}, error) { - if cached, exists := s.cache.Get("drivers"); exists { - return cached, nil - } - - logging.Debug("Loading driver categories from database") - categories := s.repository.GetDriverCategories(ctx.UserContext()) - s.cache.Set("drivers", categories) - return categories, nil + logging.Debug("Getting driver categories") + return s.repository.GetDriverCategories(ctx.UserContext()) } func (s *LookupService) GetCupCategories(ctx *fiber.Ctx) (interface{}, error) { - if cached, exists := s.cache.Get("cups"); exists { - return cached, nil - } - - logging.Debug("Loading cup categories from database") - categories := s.repository.GetCupCategories(ctx.UserContext()) - s.cache.Set("cups", categories) - return categories, nil + logging.Debug("Getting cup categories") + return s.repository.GetCupCategories(ctx.UserContext()) } func (s *LookupService) GetSessionTypes(ctx *fiber.Ctx) (interface{}, error) { - if cached, exists := s.cache.Get("sessions"); exists { - return cached, nil - } - - logging.Debug("Loading session types from database") - types := s.repository.GetSessionTypes(ctx.UserContext()) - s.cache.Set("sessions", types) - return types, nil -} - -// ClearCache clears all cached lookup data -func (s *LookupService) ClearCache() { - logging.Debug("Clearing all lookup cache data") - s.cache.Clear() -} - -// PreloadCache loads all lookup data into cache -func (s *LookupService) PreloadCache(ctx *fiber.Ctx) { - logging.Debug("Preloading all lookup cache data") - s.GetTracks(ctx) - s.GetCarModels(ctx) - s.GetDriverCategories(ctx) - s.GetCupCategories(ctx) - s.GetSessionTypes(ctx) - logging.Debug("Completed preloading lookup cache data") + logging.Debug("Getting session types") + return s.repository.GetSessionTypes(ctx.UserContext()) } diff --git a/local/service/service.go b/local/service/service.go index 94f410b..ec2edd0 100644 --- a/local/service/service.go +++ b/local/service/service.go @@ -1,10 +1,8 @@ package service import ( - "acc-server-manager/local/model" "acc-server-manager/local/repository" "acc-server-manager/local/utl/logging" - "context" "go.uber.org/dig" ) @@ -18,12 +16,6 @@ func InitializeServices(c *dig.Container) { logging.Debug("Initializing repositories") repository.InitializeRepositories(c) - // Provide caches - logging.Debug("Creating lookup cache instance") - c.Provide(func() *model.LookupCache { - return model.NewLookupCache() - }) - logging.Debug("Registering services") // Provide services c.Provide(NewServerService) @@ -37,26 +29,12 @@ func InitializeServices(c *dig.Container) { c.Provide(NewFirewallService) logging.Debug("Initializing service dependencies") - err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, lookup *LookupService, systemConfig *SystemConfigService) { + err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, systemConfig *SystemConfigService) { logging.Debug("Setting up service cross-references") api.SetServerService(server) config.SetServerService(server) - - logging.Debug("Initializing lookup data cache") - // Initialize lookup data using repository directly - lookup.cache.Set("tracks", lookup.repository.GetTracks(context.Background())) - lookup.cache.Set("cars", lookup.repository.GetCarModels(context.Background())) - lookup.cache.Set("drivers", lookup.repository.GetDriverCategories(context.Background())) - lookup.cache.Set("cups", lookup.repository.GetCupCategories(context.Background())) - lookup.cache.Set("sessions", lookup.repository.GetSessionTypes(context.Background())) - logging.Debug("Completed initializing lookup data cache") - logging.Debug("Initializing system config service") - // Initialize system config service - if err := systemConfig.Initialize(context.Background()); err != nil { - logging.Panic("failed to initialize system config service: " + err.Error()) - } - logging.Debug("Completed initializing system config service") + }) if err != nil { logging.Panic("unable to initialize services: " + err.Error()) diff --git a/local/service/state_history.go b/local/service/state_history.go index 953419b..88c0920 100644 --- a/local/service/state_history.go +++ b/local/service/state_history.go @@ -3,11 +3,11 @@ package service import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" - "acc-server-manager/local/utl/logging" - "sort" - "time" + "acc-server-manager/pkg/logging" + "sync" "github.com/gofiber/fiber/v2" + "golang.org/x/sync/errgroup" ) type StateHistoryService struct { @@ -15,18 +15,9 @@ type StateHistoryService struct { } func NewStateHistoryService(repository *repository.StateHistoryRepository) *StateHistoryService { - return &StateHistoryService{ - repository: repository, - } + return &StateHistoryService{repository: repository} } -// GetAll -// Gets All rows from StateHistory table. -// -// Args: -// context.Context: Application context -// Returns: -// string: Application version func (s *StateHistoryService) GetAll(ctx *fiber.Ctx, filter *model.StateHistoryFilter) (*[]model.StateHistory, error) { result, err := s.repository.GetAll(ctx.UserContext(), filter) if err != nil { @@ -44,168 +35,99 @@ func (s *StateHistoryService) Insert(ctx *fiber.Ctx, model *model.StateHistory) return nil } +func (s *StateHistoryService) GetLastSessionID(ctx *fiber.Ctx, serverID uint) (uint, error) { + return s.repository.GetLastSessionID(ctx.UserContext(), serverID) +} + func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateHistoryFilter) (*model.StateHistoryStats, error) { - // Get all state history entries based on filter - entries, err := s.repository.GetAll(ctx.UserContext(), filter) - if err != nil { - logging.Error("Error getting state history for statistics: %v", err) + stats := &model.StateHistoryStats{} + var mu sync.Mutex + + eg, gCtx := errgroup.WithContext(ctx.UserContext()) + + // Get Summary Stats (Peak/Avg Players, Total Sessions) + eg.Go(func() error { + summary, err := s.repository.GetSummaryStats(gCtx, filter) + if err != nil { + logging.Error("Error getting summary stats: %v", err) + return err + } + mu.Lock() + stats.PeakPlayers = summary.PeakPlayers + stats.AveragePlayers = summary.AveragePlayers + stats.TotalSessions = summary.TotalSessions + mu.Unlock() + return nil + }) + + // Get Total Playtime + eg.Go(func() error { + playtime, err := s.repository.GetTotalPlaytime(gCtx, filter) + if err != nil { + logging.Error("Error getting total playtime: %v", err) + return err + } + mu.Lock() + stats.TotalPlaytime = playtime + mu.Unlock() + return nil + }) + + // Get Player Count Over Time + eg.Go(func() error { + playerCount, err := s.repository.GetPlayerCountOverTime(gCtx, filter) + if err != nil { + logging.Error("Error getting player count over time: %v", err) + return err + } + mu.Lock() + stats.PlayerCountOverTime = playerCount + mu.Unlock() + return nil + }) + + // Get Session Types + eg.Go(func() error { + sessionTypes, err := s.repository.GetSessionTypes(gCtx, filter) + if err != nil { + logging.Error("Error getting session types: %v", err) + return err + } + mu.Lock() + stats.SessionTypes = sessionTypes + mu.Unlock() + return nil + }) + + // Get Daily Activity + eg.Go(func() error { + dailyActivity, err := s.repository.GetDailyActivity(gCtx, filter) + if err != nil { + logging.Error("Error getting daily activity: %v", err) + return err + } + mu.Lock() + stats.DailyActivity = dailyActivity + mu.Unlock() + return nil + }) + + // Get Recent Sessions + eg.Go(func() error { + recentSessions, err := s.repository.GetRecentSessions(gCtx, filter) + if err != nil { + logging.Error("Error getting recent sessions: %v", err) + return err + } + mu.Lock() + stats.RecentSessions = recentSessions + mu.Unlock() + return nil + }) + + if err := eg.Wait(); err != nil { return nil, err } - stats := &model.StateHistoryStats{ - PlayerCountOverTime: make([]model.PlayerCountPoint, 0), - SessionTypes: make([]model.SessionCount, 0), - DailyActivity: make([]model.DailyActivity, 0), - RecentSessions: make([]model.RecentSession, 0), - } - - if len(*entries) == 0 { - return stats, nil - } - - // Maps to track unique sessions and their details - sessionMap := make(map[uint]*struct { - StartTime time.Time - EndTime time.Time - Session string - Track string - MaxPlayers int - SessionConcluded bool - }) - - // Maps for aggregating statistics - dailySessionCount := make(map[string]int) - sessionTypeCount := make(map[string]int) - totalPlayers := 0 - peakPlayers := 0 - - // Process each state history entry - for _, entry := range *entries { - // Track player count over time - stats.PlayerCountOverTime = append(stats.PlayerCountOverTime, model.PlayerCountPoint{ - Timestamp: entry.DateCreated, - Count: entry.PlayerCount, - }) - - // Update peak players - if entry.PlayerCount > peakPlayers { - peakPlayers = entry.PlayerCount - } - - totalPlayers += entry.PlayerCount - - // Process session information using SessionID - if _, exists := sessionMap[entry.SessionID]; !exists { - sessionMap[entry.SessionID] = &struct { - StartTime time.Time - EndTime time.Time - Session string - Track string - MaxPlayers int - SessionConcluded bool - }{ - StartTime: entry.DateCreated, - Session: entry.Session, - Track: entry.Track, - MaxPlayers: entry.PlayerCount, - SessionConcluded: false, - } - - // Count session types - sessionTypeCount[entry.Session]++ - - // Count daily sessions - dateStr := entry.DateCreated.Format("2006-01-02") - dailySessionCount[dateStr]++ - } else { - session := sessionMap[entry.SessionID] - if session.SessionConcluded { - continue - } - if (entry.PlayerCount == 0) { - session.SessionConcluded = true - } - session.EndTime = entry.DateCreated - if entry.PlayerCount > session.MaxPlayers { - session.MaxPlayers = entry.PlayerCount - } - } - } - - for key, session := range sessionMap { - if !session.SessionConcluded { - session.SessionConcluded = true - } - if (session.MaxPlayers == 0) { - delete(sessionMap, key) - } - } - - // Calculate statistics - stats.PeakPlayers = peakPlayers - stats.TotalSessions = len(sessionMap) - if len(*entries) > 0 { - stats.AveragePlayers = float64(totalPlayers) / float64(len(*entries)) - } - - // Process session types - for sessionType, count := range sessionTypeCount { - stats.SessionTypes = append(stats.SessionTypes, model.SessionCount{ - Name: sessionType, - Count: count, - }) - } - - // Process daily activity - for dateStr, count := range dailySessionCount { - date, _ := time.Parse("2006-01-02", dateStr) - stats.DailyActivity = append(stats.DailyActivity, model.DailyActivity{ - Date: date, - SessionsCount: count, - }) - } - - // Calculate total playtime and prepare recent sessions - var recentSessions []model.RecentSession - totalPlaytime := 0 - - for sessionID, session := range sessionMap { - if !session.EndTime.IsZero() { - duration := int(session.EndTime.Sub(session.StartTime).Minutes()) - totalPlaytime += duration - - recentSessions = append(recentSessions, model.RecentSession{ - ID: sessionID, - Date: session.StartTime, - Type: session.Session, - Track: session.Track, - Duration: duration, - Players: session.MaxPlayers, - }) - } - } - - stats.TotalPlaytime = totalPlaytime - - // Sort recent sessions by date (newest first) and limit to last 10 - sort.Slice(recentSessions, func(i, j int) bool { - return recentSessions[i].Date.After(recentSessions[j].Date) - }) - - if len(recentSessions) > 10 { - recentSessions = recentSessions[:10] - } - stats.RecentSessions = recentSessions - - // Sort daily activity by date - sort.Slice(stats.DailyActivity, func(i, j int) bool { - return stats.DailyActivity[i].Date.Before(stats.DailyActivity[j].Date) - }) - - // Sort player count over time by timestamp - sort.Slice(stats.PlayerCountOverTime, func(i, j int) bool { - return stats.PlayerCountOverTime[i].Timestamp.Before(stats.PlayerCountOverTime[j].Timestamp) - }) - return stats, nil } \ No newline at end of file diff --git a/local/service/system_config_service.go b/local/service/system_config_service.go index 7faa293..eb028bd 100644 --- a/local/service/system_config_service.go +++ b/local/service/system_config_service.go @@ -3,79 +3,41 @@ package service import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" + "acc-server-manager/local/utl/cache" "acc-server-manager/local/utl/logging" "context" "fmt" "path/filepath" + "time" +) - "go.uber.org/dig" +const ( + configCacheDuration = 24 * time.Hour ) type SystemConfigService struct { repository *repository.SystemConfigRepository - cache *model.LookupCache -} - -// SystemConfigServiceParams holds the dependencies for SystemConfigService -type SystemConfigServiceParams struct { - dig.In - - Repository *repository.SystemConfigRepository - Cache *model.LookupCache + cache *cache.InMemoryCache } // NewSystemConfigService creates a new SystemConfigService with dependencies injected by dig -func NewSystemConfigService(params SystemConfigServiceParams) *SystemConfigService { +func NewSystemConfigService(repository *repository.SystemConfigRepository, cache *cache.InMemoryCache) *SystemConfigService { logging.Debug("Initializing SystemConfigService") return &SystemConfigService{ - repository: params.Repository, - cache: params.Cache, + repository: repository, + cache: cache, } } -func (s *SystemConfigService) Initialize(ctx context.Context) error { - logging.Debug("Initializing system config cache") - // Cache all configs - configs, err := s.repository.GetAll(ctx) - if err != nil { - return fmt.Errorf("failed to get configs for caching: %v", err) - } - - for _, config := range *configs { - cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, config.Key) - s.cache.Set(cacheKey, &config) - logging.Debug("Cached system config: %s", config.Key) - } - - logging.Debug("Completed initializing system config cache") - return nil -} - func (s *SystemConfigService) GetConfig(ctx context.Context, key string) (*model.SystemConfig, error) { cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, key) - - // Try to get from cache first - if cached, exists := s.cache.Get(cacheKey); exists { - if config, ok := cached.(*model.SystemConfig); ok { - return config, nil - } - logging.Debug("Invalid type in cache for key: %s", key) + + fetcher := func() (*model.SystemConfig, error) { + logging.Debug("Loading system config from database: %s", key) + return s.repository.Get(ctx, key) } - // If not in cache, get from database - logging.Debug("Loading system config from database: %s", key) - config, err := s.repository.Get(ctx, key) - if err != nil { - return nil, err - } - if config == nil { - logging.Error("Configuration not found for key: %s", key) - return nil, nil - } - - // Cache the result - s.cache.Set(cacheKey, config) - return config, nil + return cache.GetOrSet(s.cache, cacheKey, configCacheDuration, fetcher) } func (s *SystemConfigService) GetAllConfigs(ctx context.Context) (*[]model.SystemConfig, error) { @@ -88,10 +50,10 @@ func (s *SystemConfigService) UpdateConfig(ctx context.Context, config *model.Sy return err } - // Update cache + // Invalidate cache cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, config.Key) - s.cache.Set(cacheKey, config) - logging.Debug("Updated system config in cache: %s", config.Key) + s.cache.Delete(cacheKey) + logging.Debug("Invalidated system config in cache: %s", config.Key) return nil } diff --git a/local/utl/cache/cache.go b/local/utl/cache/cache.go new file mode 100644 index 0000000..082ecac --- /dev/null +++ b/local/utl/cache/cache.go @@ -0,0 +1,102 @@ +package cache + +import ( + "sync" + "time" + + "acc-server-manager/local/utl/logging" + + "go.uber.org/dig" +) + +// CacheItem represents an item in the cache +type CacheItem struct { + Value interface{} + Expiration int64 +} + +// InMemoryCache is a thread-safe in-memory cache +type InMemoryCache struct { + items map[string]CacheItem + mu sync.RWMutex +} + +// NewInMemoryCache creates and returns a new InMemoryCache instance +func NewInMemoryCache() *InMemoryCache { + return &InMemoryCache{ + items: make(map[string]CacheItem), + } +} + +// Set adds an item to the cache with an expiration duration (in seconds) +func (c *InMemoryCache) Set(key string, value interface{}, duration time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + + var expiration int64 + if duration > 0 { + expiration = time.Now().Add(duration).UnixNano() + } + + c.items[key] = CacheItem{ + Value: value, + Expiration: expiration, + } +} + +// Get retrieves an item from the cache +func (c *InMemoryCache) Get(key string) (interface{}, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + + item, found := c.items[key] + if !found { + return nil, false + } + + if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration { + // Item has expired, but don't delete here to avoid lock upgrade. + // It will be overwritten on the next Set. + return nil, false + } + + return item.Value, true +} + +// Delete removes an item from the cache +func (c *InMemoryCache) Delete(key string) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.items, key) +} + +// GetOrSet retrieves an item from the cache. If the item is not found, it +// calls the provided function to get the value, sets it in the cache, and +// returns it. +func GetOrSet[T any](c *InMemoryCache, key string, duration time.Duration, fetcher func() (T, error)) (T, error) { + if cached, found := c.Get(key); found { + if value, ok := cached.(T); ok { + return value, nil + } + } + + value, err := fetcher() + if err != nil { + var zero T + return zero, err + } + + c.Set(key, value, duration) + return value, nil +} + +// Start initializes the cache and provides it to the DI container. +func Start(di *dig.Container) { + cache := NewInMemoryCache() + err := di.Provide(func() *InMemoryCache { + return cache + }) + if err != nil { + logging.Panic("failed to provide cache") + } +} diff --git a/local/utl/configs/configs.go b/local/utl/configs/configs.go index 6f21a63..ff6cdb6 100644 --- a/local/utl/configs/configs.go +++ b/local/utl/configs/configs.go @@ -1,8 +1,33 @@ package configs -const ( - Version = "0.0.1" - Prefix = "v1" - Secret = "Donde4sta" - SecretCode = "brasno" +import ( + "log" + "os" ) + +var ( + Version = "0.0.1" + Prefix = "v1" + Secret string + SecretCode string + EncryptionKey string +) + +func init() { + Secret = getEnv("APP_SECRET", "default-secret-for-dev-use-only") + SecretCode = getEnv("APP_SECRET_CODE", "another-secret-for-dev-use-only") + EncryptionKey = getEnv("ENCRYPTION_KEY", "a-secure-32-byte-long-key-!!!!!!") // Fallback MUST be 32 bytes for AES-256 + + if len(EncryptionKey) != 32 { + log.Fatal("ENCRYPTION_KEY must be 32 bytes long") + } +} + +// getEnv retrieves an environment variable or returns a fallback value. +func getEnv(key, fallback string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + log.Printf("Environment variable %s not set, using fallback.", key) + return fallback +} diff --git a/local/utl/db/db.go b/local/utl/db/db.go index 96c4d3d..fbaf655 100644 --- a/local/utl/db/db.go +++ b/local/utl/db/db.go @@ -3,6 +3,7 @@ package db import ( "acc-server-manager/local/model" "acc-server-manager/local/utl/logging" + "os" "time" "go.uber.org/dig" @@ -11,7 +12,12 @@ import ( ) func Start(di *dig.Container) { - db, err := gorm.Open(sqlite.Open("acc.db"), &gorm.Config{}) + dbName := os.Getenv("DB_NAME") + if dbName == "" { + dbName = "acc.db" + } + + db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) if err != nil { logging.Panic("failed to connect database") } @@ -25,50 +31,25 @@ func Start(di *dig.Container) { } func Migrate(db *gorm.DB) { - err := db.AutoMigrate(&model.ApiModel{}) + logging.Info("Migrating database") + + err := db.AutoMigrate( + &model.ApiModel{}, + &model.Config{}, + &model.Track{}, + &model.CarModel{}, + &model.CupCategory{}, + &model.DriverCategory{}, + &model.SessionType{}, + &model.StateHistory{}, + &model.SteamCredentials{}, + &model.SystemConfig{}, + ) + if err != nil { - logging.Panic("failed to migrate model.ApiModel") - } - err = db.AutoMigrate(&model.Server{}) - if err != nil { - logging.Panic("failed to migrate model.Server") - } - err = db.AutoMigrate(&model.Config{}) - if err != nil { - logging.Panic("failed to migrate model.Config") - } - err = db.AutoMigrate(&model.Track{}) - if err != nil { - logging.Panic("failed to migrate model.Track") - } - err = db.AutoMigrate(&model.CarModel{}) - if err != nil { - logging.Panic("failed to migrate model.CarModel") - } - err = db.AutoMigrate(&model.CupCategory{}) - if err != nil { - logging.Panic("failed to migrate model.CupCategory") - } - err = db.AutoMigrate(&model.DriverCategory{}) - if err != nil { - logging.Panic("failed to migrate model.DriverCategory") - } - err = db.AutoMigrate(&model.SessionType{}) - if err != nil { - logging.Panic("failed to migrate model.SessionType") - } - err = db.AutoMigrate(&model.StateHistory{}) - if err != nil { - logging.Panic("failed to migrate model.StateHistory") - } - err = db.AutoMigrate(&model.SteamCredentials{}) - if err != nil { - logging.Panic("failed to migrate model.SteamCredentials") - } - err = db.AutoMigrate(&model.SystemConfig{}) - if err != nil { - logging.Panic("failed to migrate model.SystemConfig") + logging.Panic("failed to migrate database models") } + db.FirstOrCreate(&model.ApiModel{Api: "Works"}) Seed(db) @@ -90,50 +71,13 @@ func Seed(db *gorm.DB) error { if err := seedSessionTypes(db); err != nil { return err } - if err := seedServers(db); err != nil { - return err - } - if err := seedSteamCredentials(db); err != nil { - return err - } if err := seedSystemConfigs(db); err != nil { return err } return nil } -func seedSteamCredentials(db *gorm.DB) error { - credentials := []model.SteamCredentials{ - { - ID: 1, - Username: "test", - Password: "test", - DateCreated: time.Now().UTC(), - }, - } - for _, credential := range credentials { - if err := db.FirstOrCreate(&credential).Error; err != nil { - return err - } - } - return nil -} -func seedServers(db *gorm.DB) error { - servers := []model.Server{ - {ID: 1, Name: "ACC Server - Barcelona", ServiceName: "ACC-Barcelona", Path: "C:\\steamcmd\\acc", FromSteamCMD: true}, - {ID: 2, Name: "ACC Server - Monza", ServiceName: "ACC-Monza", Path: "C:\\steamcmd\\acc2", FromSteamCMD: true}, - {ID: 3, Name: "ACC Server - Spa", ServiceName: "ACC-Spa", Path: "C:\\steamcmd\\acc3", FromSteamCMD: true}, - {ID: 4, Name: "ACC Server - League", ServiceName: "ACC-League", Path: "C:\\steamcmd\\acc-league", FromSteamCMD: true}, - } - - for _, track := range servers { - if err := db.FirstOrCreate(&track).Error; err != nil { - return err - } - } - return nil -} func seedTracks(db *gorm.DB) error { tracks := []model.Track{ diff --git a/local/utl/server/server.go b/local/utl/server/server.go index 45339f6..826549f 100644 --- a/local/utl/server/server.go +++ b/local/utl/server/server.go @@ -3,32 +3,31 @@ package server import ( "acc-server-manager/local/api" "acc-server-manager/local/utl/logging" - "fmt" "os" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/helmet" "github.com/gofiber/swagger" "go.uber.org/dig" ) func Start(di *dig.Container) *fiber.App { - // Initialize logger - logger, err := logging.Initialize() - if err != nil { - fmt.Printf("Failed to initialize logger: %v\n", err) - os.Exit(1) - } - defer logger.Close() - - // Set up panic recovery - defer logging.RecoverAndLog() - app := fiber.New(fiber.Config{ EnablePrintRoutes: true, }) - app.Use(cors.New()) + app.Use(helmet.New()) + + allowedOrigin := os.Getenv("CORS_ALLOWED_ORIGIN") + if allowedOrigin == "" { + allowedOrigin = "http://localhost:5173" + } + + app.Use(cors.New(cors.Config{ + AllowOrigins: allowedOrigin, + AllowHeaders: "Origin, Content-Type, Accept", + })) app.Get("/swagger/*", swagger.HandlerDefault)