security measures

This commit is contained in:
Fran Jurmanović
2025-06-25 22:37:38 +02:00
parent 1ecd558e18
commit 69733e4940
16 changed files with 614 additions and 506 deletions

View File

@@ -1,8 +1,12 @@
package main package main
import ( import (
"acc-server-manager/local/utl/cache"
"acc-server-manager/local/utl/db" "acc-server-manager/local/utl/db"
"acc-server-manager/local/utl/logging"
"acc-server-manager/local/utl/server" "acc-server-manager/local/utl/server"
"fmt"
"os"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"go.uber.org/dig" "go.uber.org/dig"
@@ -12,7 +16,19 @@ import (
func main() { func main() {
godotenv.Load() 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() di := dig.New()
cache.Start(di)
db.Start(di) db.Start(di)
server.Start(di) server.Start(di)
} }

15
go.mod
View File

@@ -1,15 +1,16 @@
module acc-server-manager module acc-server-manager
go 1.22.3 go 1.23.0
require ( 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/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/joho/godotenv v1.5.1
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c
github.com/swaggo/swag v1.16.3 github.com/swaggo/swag v1.16.3
go.uber.org/dig v1.17.1 go.uber.org/dig v1.17.1
golang.org/x/sync v0.15.0
golang.org/x/text v0.16.0 golang.org/x/text v0.16.0
gorm.io/driver/sqlite v1.5.6 gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.11 gorm.io/gorm v1.25.11
@@ -17,7 +18,7 @@ require (
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect 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/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec 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/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // 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/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/mattn/go-sqlite3 v1.14.22 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.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 golang.org/x/tools v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

28
go.sum
View File

@@ -1,7 +1,7 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= 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/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 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 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.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 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 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ=
github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8= 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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 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/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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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= 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= 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 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=

View File

@@ -9,6 +9,7 @@ import (
) )
type IntString int type IntString int
type IntBool int
// Config tracks configuration modifications // Config tracks configuration modifications
type Config struct { type Config struct {
@@ -90,11 +91,11 @@ type EventRules struct {
DriverStIntStringTimeSec IntString `json:"driverStIntStringTimeSec"` DriverStIntStringTimeSec IntString `json:"driverStIntStringTimeSec"`
MandatoryPitstopCount IntString `json:"mandatoryPitstopCount"` MandatoryPitstopCount IntString `json:"mandatoryPitstopCount"`
MaxTotalDrivingTime IntString `json:"maxTotalDrivingTime"` MaxTotalDrivingTime IntString `json:"maxTotalDrivingTime"`
IsRefuellingAllowedInRace bool `json:"isRefuellingAllowedInRace"` IsRefuellingAllowedInRace IntBool `json:"isRefuellingAllowedInRace"`
IsRefuellingTimeFixed bool `json:"isRefuellingTimeFixed"` IsRefuellingTimeFixed IntBool `json:"isRefuellingTimeFixed"`
IsMandatoryPitstopRefuellingRequired bool `json:"isMandatoryPitstopRefuellingRequired"` IsMandatoryPitstopRefuellingRequired IntBool `json:"isMandatoryPitstopRefuellingRequired"`
IsMandatoryPitstopTyreChangeRequired bool `json:"isMandatoryPitstopTyreChangeRequired"` IsMandatoryPitstopTyreChangeRequired IntBool `json:"isMandatoryPitstopTyreChangeRequired"`
IsMandatoryPitstopSwapDriverRequired bool `json:"isMandatoryPitstopSwapDriverRequired"` IsMandatoryPitstopSwapDriverRequired IntBool `json:"isMandatoryPitstopSwapDriverRequired"`
TyreSetCount IntString `json:"tyreSetCount"` TyreSetCount IntString `json:"tyreSetCount"`
} }
@@ -127,6 +128,34 @@ const (
CacheKeySystemConfig = "system_config_%s" // Format with config key 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 { func (i *IntString) UnmarshalJSON(b []byte) error {
var str string var str string
if err := json.Unmarshal(b, &str); err == nil { if err := json.Unmarshal(b, &str); err == nil {
@@ -148,7 +177,7 @@ func (i *IntString) UnmarshalJSON(b []byte) error {
return nil return nil
} }
return fmt.Errorf("invalid postQualySeconds value") return fmt.Errorf("invalid IntString value")
} }
func (i IntString) ToString() string { func (i IntString) ToString() string {

View File

@@ -1,6 +1,7 @@
package model package model
import ( import (
"acc-server-manager/local/utl/configs"
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/rand" "crypto/rand"
@@ -84,12 +85,10 @@ func (s *SteamCredentials) Validate() error {
return nil return nil
} }
// GetEncryptionKey returns the encryption key, in a real application this should be stored securely // GetEncryptionKey returns the encryption key from config.
// and potentially rotated periodically // The key is loaded from the ENCRYPTION_KEY environment variable.
func GetEncryptionKey() []byte { func GetEncryptionKey() []byte {
// This is a placeholder - in production, this should be stored securely return []byte(configs.EncryptionKey)
// and potentially fetched from a key management service
return []byte("your-32-byte-encryption-key-here")
} }
// EncryptPassword encrypts a password using AES-256 // EncryptPassword encrypts a password using AES-256

View File

@@ -2,87 +2,90 @@ package repository
import ( import (
"acc-server-manager/local/model" "acc-server-manager/local/model"
"acc-server-manager/local/utl/cache"
"context" "context"
"time"
"gorm.io/gorm" "gorm.io/gorm"
) )
const (
cacheDuration = 1 * time.Hour
tracksCacheKey = "tracks"
carModelsCacheKey = "carModels"
driverCategoriesCacheKey = "driverCategories"
cupCategoriesCacheKey = "cupCategories"
sessionTypesCacheKey = "sessionTypes"
)
type LookupRepository struct { 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{ return &LookupRepository{
db: db, db: db,
cache: cache,
} }
} }
// GetTracks func (r *LookupRepository) GetTracks(ctx context.Context) (*[]model.Track, error) {
// Gets Tracks rows from Lookup table. fetcher := func() (*[]model.Track, error) {
// db := r.db.WithContext(ctx)
// Args: items := new([]model.Track)
// context.Context: Application context if err := db.Find(items).Error; err != nil {
// Returns: return nil, err
// model.LookupModel: Lookup object from database. }
func (as LookupRepository) GetTracks(ctx context.Context) *[]model.Track { return items, nil
db := as.db.WithContext(ctx) }
TrackModel := new([]model.Track) return cache.GetOrSet(r.cache, tracksCacheKey, cacheDuration, fetcher)
db.Find(&TrackModel)
return TrackModel
} }
// GetCarModels func (r *LookupRepository) GetCarModels(ctx context.Context) (*[]model.CarModel, error) {
// Gets CarModels rows from Lookup table. fetcher := func() (*[]model.CarModel, error) {
// db := r.db.WithContext(ctx)
// Args: items := new([]model.CarModel)
// context.Context: Application context if err := db.Find(items).Error; err != nil {
// Returns: return nil, err
// model.LookupModel: Lookup object from database. }
func (as LookupRepository) GetCarModels(ctx context.Context) *[]model.CarModel { return items, nil
db := as.db.WithContext(ctx) }
CarModelModel := new([]model.CarModel) return cache.GetOrSet(r.cache, carModelsCacheKey, cacheDuration, fetcher)
db.Find(&CarModelModel)
return CarModelModel
} }
// GetDriverCategories func (r *LookupRepository) GetDriverCategories(ctx context.Context) (*[]model.DriverCategory, error) {
// Gets DriverCategories rows from Lookup table. fetcher := func() (*[]model.DriverCategory, error) {
// db := r.db.WithContext(ctx)
// Args: items := new([]model.DriverCategory)
// context.Context: Application context if err := db.Find(items).Error; err != nil {
// Returns: return nil, err
// model.LookupModel: Lookup object from database. }
func (as LookupRepository) GetDriverCategories(ctx context.Context) *[]model.DriverCategory { return items, nil
db := as.db.WithContext(ctx) }
DriverCategoryModel := new([]model.DriverCategory) return cache.GetOrSet(r.cache, driverCategoriesCacheKey, cacheDuration, fetcher)
db.Find(&DriverCategoryModel)
return DriverCategoryModel
} }
// GetCupCategories func (r *LookupRepository) GetCupCategories(ctx context.Context) (*[]model.CupCategory, error) {
// Gets CupCategories rows from Lookup table. fetcher := func() (*[]model.CupCategory, error) {
// db := r.db.WithContext(ctx)
// Args: items := new([]model.CupCategory)
// context.Context: Application context if err := db.Find(items).Error; err != nil {
// Returns: return nil, err
// model.LookupModel: Lookup object from database. }
func (as LookupRepository) GetCupCategories(ctx context.Context) *[]model.CupCategory { return items, nil
db := as.db.WithContext(ctx) }
CupCategoryModel := new([]model.CupCategory) return cache.GetOrSet(r.cache, cupCategoriesCacheKey, cacheDuration, fetcher)
db.Find(&CupCategoryModel)
return CupCategoryModel
} }
// GetSessionTypes func (r *LookupRepository) GetSessionTypes(ctx context.Context) (*[]model.SessionType, error) {
// Gets SessionTypes rows from Lookup table. fetcher := func() (*[]model.SessionType, error) {
// db := r.db.WithContext(ctx)
// Args: items := new([]model.SessionType)
// context.Context: Application context if err := db.Find(items).Error; err != nil {
// Returns: return nil, err
// model.LookupModel: Lookup object from database. }
func (as LookupRepository) GetSessionTypes(ctx context.Context) *[]model.SessionType { return items, nil
db := as.db.WithContext(ctx) }
SessionTypesModel := new([]model.SessionType) return cache.GetOrSet(r.cache, sessionTypesCacheKey, cacheDuration, fetcher)
db.Find(&SessionTypesModel)
return SessionTypesModel
} }

View File

@@ -13,11 +13,76 @@ type ServerRepository struct {
} }
func NewServerRepository(db *gorm.DB) *ServerRepository { func NewServerRepository(db *gorm.DB) *ServerRepository {
return &ServerRepository{ repo := &ServerRepository{
BaseRepository: NewBaseRepository[model.Server, model.ServerFilter](db, model.Server{}), 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 // GetFirstByServiceName
// Gets first row from Server table. // Gets first row from Server table.

View File

@@ -44,3 +44,116 @@ func (r *StateHistoryRepository) GetLastSessionID(ctx context.Context, serverID
return lastSession.SessionID, nil 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
}

View File

@@ -1,7 +1,6 @@
package service package service
import ( import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/logging"
@@ -10,85 +9,36 @@ import (
type LookupService struct { type LookupService struct {
repository *repository.LookupRepository 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") logging.Debug("Initializing LookupService")
return &LookupService{ return &LookupService{
repository: repository, repository: repository,
cache: cache,
} }
} }
func (s *LookupService) GetTracks(ctx *fiber.Ctx) (interface{}, error) { func (s *LookupService) GetTracks(ctx *fiber.Ctx) (interface{}, error) {
if cached, exists := s.cache.Get("tracks"); exists { logging.Debug("Getting tracks")
return cached, nil return s.repository.GetTracks(ctx.UserContext())
}
logging.Debug("Loading tracks from database")
tracks := s.repository.GetTracks(ctx.UserContext())
s.cache.Set("tracks", tracks)
return tracks, nil
} }
func (s *LookupService) GetCarModels(ctx *fiber.Ctx) (interface{}, error) { func (s *LookupService) GetCarModels(ctx *fiber.Ctx) (interface{}, error) {
if cached, exists := s.cache.Get("cars"); exists { logging.Debug("Getting car models")
return cached, nil return s.repository.GetCarModels(ctx.UserContext())
}
logging.Debug("Loading car models from database")
cars := s.repository.GetCarModels(ctx.UserContext())
s.cache.Set("cars", cars)
return cars, nil
} }
func (s *LookupService) GetDriverCategories(ctx *fiber.Ctx) (interface{}, error) { func (s *LookupService) GetDriverCategories(ctx *fiber.Ctx) (interface{}, error) {
if cached, exists := s.cache.Get("drivers"); exists { logging.Debug("Getting driver categories")
return cached, nil return s.repository.GetDriverCategories(ctx.UserContext())
}
logging.Debug("Loading driver categories from database")
categories := s.repository.GetDriverCategories(ctx.UserContext())
s.cache.Set("drivers", categories)
return categories, nil
} }
func (s *LookupService) GetCupCategories(ctx *fiber.Ctx) (interface{}, error) { func (s *LookupService) GetCupCategories(ctx *fiber.Ctx) (interface{}, error) {
if cached, exists := s.cache.Get("cups"); exists { logging.Debug("Getting cup categories")
return cached, nil return s.repository.GetCupCategories(ctx.UserContext())
}
logging.Debug("Loading cup categories from database")
categories := s.repository.GetCupCategories(ctx.UserContext())
s.cache.Set("cups", categories)
return categories, nil
} }
func (s *LookupService) GetSessionTypes(ctx *fiber.Ctx) (interface{}, error) { func (s *LookupService) GetSessionTypes(ctx *fiber.Ctx) (interface{}, error) {
if cached, exists := s.cache.Get("sessions"); exists { logging.Debug("Getting session types")
return cached, nil return s.repository.GetSessionTypes(ctx.UserContext())
}
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")
} }

View File

@@ -1,10 +1,8 @@
package service package service
import ( import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/logging"
"context"
"go.uber.org/dig" "go.uber.org/dig"
) )
@@ -18,12 +16,6 @@ func InitializeServices(c *dig.Container) {
logging.Debug("Initializing repositories") logging.Debug("Initializing repositories")
repository.InitializeRepositories(c) repository.InitializeRepositories(c)
// Provide caches
logging.Debug("Creating lookup cache instance")
c.Provide(func() *model.LookupCache {
return model.NewLookupCache()
})
logging.Debug("Registering services") logging.Debug("Registering services")
// Provide services // Provide services
c.Provide(NewServerService) c.Provide(NewServerService)
@@ -37,26 +29,12 @@ func InitializeServices(c *dig.Container) {
c.Provide(NewFirewallService) c.Provide(NewFirewallService)
logging.Debug("Initializing service dependencies") 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") logging.Debug("Setting up service cross-references")
api.SetServerService(server) api.SetServerService(server)
config.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 { if err != nil {
logging.Panic("unable to initialize services: " + err.Error()) logging.Panic("unable to initialize services: " + err.Error())

View File

@@ -3,11 +3,11 @@ package service
import ( import (
"acc-server-manager/local/model" "acc-server-manager/local/model"
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"acc-server-manager/local/utl/logging" "acc-server-manager/pkg/logging"
"sort" "sync"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"golang.org/x/sync/errgroup"
) )
type StateHistoryService struct { type StateHistoryService struct {
@@ -15,18 +15,9 @@ type StateHistoryService struct {
} }
func NewStateHistoryService(repository *repository.StateHistoryRepository) *StateHistoryService { func NewStateHistoryService(repository *repository.StateHistoryRepository) *StateHistoryService {
return &StateHistoryService{ return &StateHistoryService{repository: repository}
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) { func (s *StateHistoryService) GetAll(ctx *fiber.Ctx, filter *model.StateHistoryFilter) (*[]model.StateHistory, error) {
result, err := s.repository.GetAll(ctx.UserContext(), filter) result, err := s.repository.GetAll(ctx.UserContext(), filter)
if err != nil { if err != nil {
@@ -44,168 +35,99 @@ func (s *StateHistoryService) Insert(ctx *fiber.Ctx, model *model.StateHistory)
return nil 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) { func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateHistoryFilter) (*model.StateHistoryStats, error) {
// Get all state history entries based on filter stats := &model.StateHistoryStats{}
entries, err := s.repository.GetAll(ctx.UserContext(), filter) var mu sync.Mutex
if err != nil {
logging.Error("Error getting state history for statistics: %v", err) 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 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 return stats, nil
} }

View File

@@ -3,79 +3,41 @@ package service
import ( import (
"acc-server-manager/local/model" "acc-server-manager/local/model"
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"acc-server-manager/local/utl/cache"
"acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/logging"
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"time"
)
"go.uber.org/dig" const (
configCacheDuration = 24 * time.Hour
) )
type SystemConfigService struct { type SystemConfigService struct {
repository *repository.SystemConfigRepository repository *repository.SystemConfigRepository
cache *model.LookupCache cache *cache.InMemoryCache
}
// SystemConfigServiceParams holds the dependencies for SystemConfigService
type SystemConfigServiceParams struct {
dig.In
Repository *repository.SystemConfigRepository
Cache *model.LookupCache
} }
// NewSystemConfigService creates a new SystemConfigService with dependencies injected by dig // 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") logging.Debug("Initializing SystemConfigService")
return &SystemConfigService{ return &SystemConfigService{
repository: params.Repository, repository: repository,
cache: params.Cache, 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) { func (s *SystemConfigService) GetConfig(ctx context.Context, key string) (*model.SystemConfig, error) {
cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, key) cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, key)
// Try to get from cache first fetcher := func() (*model.SystemConfig, error) {
if cached, exists := s.cache.Get(cacheKey); exists { logging.Debug("Loading system config from database: %s", key)
if config, ok := cached.(*model.SystemConfig); ok { return s.repository.Get(ctx, key)
return config, nil
}
logging.Debug("Invalid type in cache for key: %s", key)
} }
// If not in cache, get from database return cache.GetOrSet(s.cache, cacheKey, configCacheDuration, fetcher)
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
} }
func (s *SystemConfigService) GetAllConfigs(ctx context.Context) (*[]model.SystemConfig, error) { 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 return err
} }
// Update cache // Invalidate cache
cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, config.Key) cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, config.Key)
s.cache.Set(cacheKey, config) s.cache.Delete(cacheKey)
logging.Debug("Updated system config in cache: %s", config.Key) logging.Debug("Invalidated system config in cache: %s", config.Key)
return nil return nil
} }

102
local/utl/cache/cache.go vendored Normal file
View File

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

View File

@@ -1,8 +1,33 @@
package configs package configs
const ( import (
Version = "0.0.1" "log"
Prefix = "v1" "os"
Secret = "Donde4sta"
SecretCode = "brasno"
) )
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
}

View File

@@ -3,6 +3,7 @@ package db
import ( import (
"acc-server-manager/local/model" "acc-server-manager/local/model"
"acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/logging"
"os"
"time" "time"
"go.uber.org/dig" "go.uber.org/dig"
@@ -11,7 +12,12 @@ import (
) )
func Start(di *dig.Container) { 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 { if err != nil {
logging.Panic("failed to connect database") logging.Panic("failed to connect database")
} }
@@ -25,50 +31,25 @@ func Start(di *dig.Container) {
} }
func Migrate(db *gorm.DB) { 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 { if err != nil {
logging.Panic("failed to migrate model.ApiModel") logging.Panic("failed to migrate database models")
}
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")
} }
db.FirstOrCreate(&model.ApiModel{Api: "Works"}) db.FirstOrCreate(&model.ApiModel{Api: "Works"})
Seed(db) Seed(db)
@@ -90,50 +71,13 @@ func Seed(db *gorm.DB) error {
if err := seedSessionTypes(db); err != nil { if err := seedSessionTypes(db); err != nil {
return err return err
} }
if err := seedServers(db); err != nil {
return err
}
if err := seedSteamCredentials(db); err != nil {
return err
}
if err := seedSystemConfigs(db); err != nil { if err := seedSystemConfigs(db); err != nil {
return err return err
} }
return nil 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 { func seedTracks(db *gorm.DB) error {
tracks := []model.Track{ tracks := []model.Track{

View File

@@ -3,32 +3,31 @@ package server
import ( import (
"acc-server-manager/local/api" "acc-server-manager/local/api"
"acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/logging"
"fmt"
"os" "os"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/gofiber/swagger" "github.com/gofiber/swagger"
"go.uber.org/dig" "go.uber.org/dig"
) )
func Start(di *dig.Container) *fiber.App { 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{ app := fiber.New(fiber.Config{
EnablePrintRoutes: true, 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) app.Get("/swagger/*", swagger.HandlerDefault)