security measures
This commit is contained in:
@@ -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
15
go.mod
@@ -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
28
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
102
local/utl/cache/cache.go
vendored
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user