From 16d782fbe85f76bf361675840b0b599d32f8c67a Mon Sep 17 00:00:00 2001 From: MaxJa4 <74194322+MaxJa4@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:49:20 +0100 Subject: [PATCH] Bugfixes. Optimizations/refactor. Add redis for player-cache. Add docker files. Replace sqlite dep. Single-Calc for existing players. Game-Metrics in JSON. --- .dockerignore | 6 + .gitignore | 1 + .idea/inspectionProfiles/Go_Error.xml | 427 ++++++++++++++++++++++ .idea/runConfigurations/Redis.xml | 9 + Dockerfile | 24 ++ auth.go | 27 +- config/metrics.json | 88 +++++ controllers/cache_controller.go | 136 ++++--- controllers/metrics_controller.go | 32 ++ controllers/player_controller.go | 21 +- controllers/user_settings_controller.go | 43 ++- docker-compose.yml | 18 + go.mod | 21 +- go.sum | 28 +- go.work | 7 + go.work.sum | 31 ++ internal/cache/cache.go | 55 +++ internal/cache/go.mod | 3 + internal/session/go.mod | 3 + internal/session/session.go | 43 +++ main.go | 15 +- models/cache.go | 11 - models/metric_setting.go | 9 - models/metrics_json.go | 18 + models/setup.go | 30 +- models/tracker_json.go | 16 + pages.go | 27 +- static/index.css | 3 +- static/index.js | 12 +- templates/components/bottom_controls.html | 4 +- templates/modals/add_clan.html | 4 +- templates/modals/add_player.html | 12 +- templates/modals/delete_clan.html | 1 + templates/modals/delete_player.html | 6 +- templates/modals/edit_clan.html | 4 +- templates/modals/edit_player.html | 8 +- templates/modals/settings.html | 4 +- templates/player_list_item.html | 8 +- utils/globals.go | 6 +- utils/score.go | 129 +++++++ utils/session.go | 7 - 41 files changed, 1154 insertions(+), 203 deletions(-) create mode 100644 .dockerignore create mode 100644 .idea/inspectionProfiles/Go_Error.xml create mode 100644 Dockerfile create mode 100644 config/metrics.json create mode 100644 controllers/metrics_controller.go create mode 100644 docker-compose.yml create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 internal/cache/cache.go create mode 100644 internal/cache/go.mod create mode 100644 internal/session/go.mod create mode 100644 internal/session/session.go delete mode 100644 models/cache.go delete mode 100644 models/metric_setting.go create mode 100644 models/metrics_json.go create mode 100644 models/tracker_json.go create mode 100644 utils/score.go delete mode 100644 utils/session.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3592d05 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.db +.log +redis +Dockerfile +docker-compose.yml +.gitignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2713e6a..1f664fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /isc_rest.log /isc_data.db +redis \ No newline at end of file diff --git a/.idea/inspectionProfiles/Go_Error.xml b/.idea/inspectionProfiles/Go_Error.xml new file mode 100644 index 0000000..7ebb3e4 --- /dev/null +++ b/.idea/inspectionProfiles/Go_Error.xml @@ -0,0 +1,427 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Redis.xml b/.idea/runConfigurations/Redis.xml index b91b9ae..1264935 100644 --- a/.idea/runConfigurations/Redis.xml +++ b/.idea/runConfigurations/Redis.xml @@ -3,6 +3,7 @@ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fe96d36 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum internal/**/go.mod ./ + +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o isc . + +FROM alpine:latest + +WORKDIR /root/ + +COPY --from=builder /app/isc . +COPY --from=builder /app/templates ./templates +COPY --from=builder /app/static ./static +COPY --from=builder /app/config ./config + +EXPOSE 8000 + +CMD ["./isc"] \ No newline at end of file diff --git a/auth.go b/auth.go index 8556d60..6a62525 100644 --- a/auth.go +++ b/auth.go @@ -3,12 +3,12 @@ package main import ( "InfantrySkillCalculator/controllers" "InfantrySkillCalculator/models" - "InfantrySkillCalculator/utils" "errors" "fmt" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" + "internal/session" "log" "net/http" ) @@ -48,25 +48,24 @@ func hashPassword(password string) (string, error) { func AuthRequired() gin.HandlerFunc { return func(c *gin.Context) { - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - if auth, ok := session.Values["authenticated"].(bool); !ok || !auth || !controllers.IsUserEnabled(session.Values["username"].(string)) { + auth, okAuth := session.GetAuthenticated(c) + username, okUser := session.GetUsername(c) + + if !okAuth || !okUser || !auth || !controllers.IsUserEnabled(username) { redirectToLogin(c) return } + c.Next() } } func AdminAuthRequired() gin.HandlerFunc { return func(c *gin.Context) { - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { - redirectToLogin(c) - return - } + auth, okAuth := session.GetAuthenticated(c) + username, okUser := session.GetUsername(c) - username, ok := session.Values["username"].(string) - if !ok || !controllers.IsUserEnabled(username) || !controllers.IsUserAdmin(username) { + if !okAuth || !okUser || !auth || !controllers.IsUserEnabled(username) || !controllers.IsUserAdmin(username) { redirectToLogin(c) return } @@ -76,8 +75,7 @@ func AdminAuthRequired() gin.HandlerFunc { } func isUserAdmin(c *gin.Context) bool { - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - username, ok := session.Values["username"].(string) + username, ok := session.GetUsername(c) if !ok { return false } @@ -85,10 +83,7 @@ func isUserAdmin(c *gin.Context) bool { } func redirectToLogin(c *gin.Context) { - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - session.Options.MaxAge = -1 - err := session.Save(c.Request, c.Writer) - if err != nil { + if err := session.InvalidateSession(c); err != nil { log.Fatal(err) } c.Redirect(http.StatusFound, "/login") diff --git a/config/metrics.json b/config/metrics.json new file mode 100644 index 0000000..0dc828c --- /dev/null +++ b/config/metrics.json @@ -0,0 +1,88 @@ +{ + "GameMetrics": [ + { + "NormalizeFactor": 17.7, + "TopWeaponCount": 3, + "GameName": "BF5", + "WeaponMetrics": [ + { + "WeaponCategory": "Semi-auto rifle", + "AccuracyFactor": 1.42, + "KpmFactor": 1.19 + }, + { + "WeaponCategory": "Self-loading rifle", + "AccuracyFactor": 2.05, + "KpmFactor": 1.19 + }, + { + "WeaponCategory": "Bolt action rifle", + "AccuracyFactor": 2.07, + "KpmFactor": 1.0 + }, + { + "WeaponCategory": "Shotgun", + "AccuracyFactor": 5.59, + "KpmFactor": 0.934 + }, + { + "WeaponCategory": "Assault Rifles", + "AccuracyFactor": 1.09, + "KpmFactor": 1.04 + }, + { + "WeaponCategory": "Pistol carbine", + "AccuracyFactor": 1.13, + "KpmFactor": 1.14 + }, + { + "WeaponCategory": "Bolt action carbine", + "AccuracyFactor": 1.74, + "KpmFactor": 0.737 + }, + { + "WeaponCategory": "Smg", + "AccuracyFactor": 0.966, + "KpmFactor": 1.04 + }, + { + "WeaponCategory": "LMG", + "AccuracyFactor": 0.944, + "KpmFactor": 0.919 + }, + { + "WeaponCategory": "Assault Rifle", + "AccuracyFactor": 1.09, + "KpmFactor": 1.04 + }, + { + "WeaponCategory": "Bolt Action", + "AccuracyFactor": 2.07, + "KpmFactor": 1.0 + } + ] + }, + { + "NormalizeFactor": 17.8, + "TopWeaponCount": 3, + "GameName": "BF2042", + "WeaponMetrics": [ + { + "WeaponCategory": "Assault Rifles", + "AccuracyFactor": 1.0, + "KpmFactor": 1.0 + }, + { + "WeaponCategory": "LMG", + "AccuracyFactor": 0.8, + "KpmFactor": 1.238 + }, + { + "WeaponCategory": "PDW", + "AccuracyFactor": 0.92, + "KpmFactor": 0.963 + } + ] + } + ] +} \ No newline at end of file diff --git a/controllers/cache_controller.go b/controllers/cache_controller.go index 08da605..b140f60 100644 --- a/controllers/cache_controller.go +++ b/controllers/cache_controller.go @@ -3,37 +3,29 @@ package controllers import ( "InfantrySkillCalculator/models" "InfantrySkillCalculator/utils" - "context" - "errors" "fmt" "github.com/gin-gonic/gin" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" - "gorm.io/gorm/clause" + "internal/cache" + "log" "net/http" - "time" ) type AddCacheInput struct { - PlayerID uint `json:"player_id" binding:"required"` - CacheDate time.Time `json:"date" binding:"required"` - Score float32 `json:"score" gorm:"default:-1.0"` - Game string `json:"game" binding:"required"` + PlayerID uint `json:"player_id" binding:"required"` + Score float32 `json:"score" gorm:"default:-1.0"` + GameTag string `json:"game_tag" binding:"required"` } type UpdateCacheInput struct { - CacheDate time.Time `json:"date" binding:"required"` - Score float32 `json:"score" gorm:"default:-1.0"` + Score float32 `json:"score" gorm:"default:-1.0"` } -var ctx = context.Background() - -// GetCacheByPlayerID GET /cache/:player_id?game_id=TAG +// GetCacheByPlayerID GET /cache/:player_id?game_tag=TAG func GetCacheByPlayerID(c *gin.Context) { playerId := utils.StringToUint(c.Param("player_id")) - gameId := utils.StringToUint(c.Request.URL.Query().Get("game_id")) + gameTag := c.Request.URL.Query().Get("game_tag") - val, err := GetCacheByPlayerIDGorm(playerId, gameId) + val, err := cache.GetScore(playerId, gameTag) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) } else { @@ -41,19 +33,6 @@ func GetCacheByPlayerID(c *gin.Context) { } } -func GetCacheByPlayerIDGorm(playerId uint, gameId uint) (float32, error) { - key := fmt.Sprintf("player:%d:game:%d", playerId, gameId) - - val, err := models.Cache.Get(ctx, key).Result() - if errors.Is(err, redis.Nil) { - return -1.0, nil // cache miss - } else if err != nil { - return -1.0, err // cache error - } else { - return utils.StringToFloat(val), nil // cache hit - } -} - // AddCache POST /cache func AddCache(c *gin.Context) { var input AddCacheInput @@ -63,69 +42,84 @@ func AddCache(c *gin.Context) { } var game models.Game - if err := FindGameByTag(&game, input.Game).Error; err != nil { + if err := FindGameByTag(&game, input.GameTag).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Game not found!"}) return } - cache := models.PlayerCache{CacheDate: input.CacheDate, PlayerID: input.PlayerID, Score: input.Score, Game: input.Game} + err := cache.SetScore(input.PlayerID, input.GameTag, input.Score) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()}) + return + } - c.JSON(http.StatusOK, cache) + c.JSON(http.StatusOK, nil) } // UpdateCacheByPlayerID PATCH /cache/:id?game=TAG func UpdateCacheByPlayerID(c *gin.Context) { - var cache models.PlayerCache - if err := FindCacheGin(&cache, c).Error; err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) - return + playerID := utils.StringToUint(c.Param("id")) + gameTag := c.Request.URL.Query().Get("game") + score := utils.StringToFloat(c.PostForm("score")) + + err := cache.SetScore(playerID, gameTag, score) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()}) + } else { + c.JSON(http.StatusOK, nil) } - - var input UpdateCacheInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - models.DB.Model(&cache).Updates(map[string]interface{}{ - "CacheDate": input.CacheDate, - "Score": input.Score, - }) - - c.JSON(http.StatusOK, cache) } // DeleteCacheByPlayerID DELETE /cache/:id?game=TAG func DeleteCacheByPlayerID(c *gin.Context) { - var cache models.PlayerCache - if err := FindCacheGin(&cache, c).Error; err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) - return + playerID := utils.StringToUint(c.Param("id")) + gameTag := c.Request.URL.Query().Get("game") + + if err := cache.DeleteScore(playerID, gameTag); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } else { + c.JSON(http.StatusOK, nil) } - - models.DB.Delete(&cache) - - c.JSON(http.StatusOK, true) } // DeleteAllCaches DELETE /cache func DeleteAllCaches(c *gin.Context) { - var caches []models.PlayerCache + if err := cache.PurgeCache(); err != nil { + c.String(http.StatusBadRequest, err.Error()) + } else { + c.String(http.StatusOK, "Purged all caches!") + } +} + +// GetScoreByPlayerID GET /score/:player_id?game_tag=TAG +func GetScoreByPlayerID(c *gin.Context) { + var player models.Player + var gameTag = c.Request.URL.Query().Get("game_tag") + var playerId = utils.StringToUint(c.Param("player_id")) if err := models.DB. - Session(&gorm.Session{AllowGlobalUpdate: true}). - Clauses(clause.Returning{}). - Delete(&caches).Error; err != nil { - c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error()) + Model(&models.Player{}). + Where("id = ?", playerId). + First(&player).Error; err != nil { + + c.JSON(http.StatusBadRequest, gin.H{"error": "Player not found!"}) return } - c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(caches)))+" caches!") -} + score, err := cache.GetScore(player.ID, gameTag) + if err != nil || score == -1 { + score = utils.CalcPlayerScore(player.Name, gameTag) + if score == score && score != -1 { // not NaN + if err := cache.SetScore(player.ID, gameTag, score); err != nil { + log.Fatal(err) + } + } + } -func FindCacheGin(out interface{}, c *gin.Context) *gorm.DB { - return FindCache(out, utils.StringToUint(c.Param("id")), c.Request.URL.Query().Get("game")) -} - -func FindCache(out interface{}, id uint, game string) *gorm.DB { - return models.DB.Where("player_id = ?", id).Where("game = ?", game).First(out) + if score != score || score == -1 { // NaN + c.String(http.StatusOK, "") + } else if score != -1 { + c.String(http.StatusOK, fmt.Sprintf("%.2f", score)) + } else { + c.JSON(http.StatusBadRequest, "Invalid request!") + } } diff --git a/controllers/metrics_controller.go b/controllers/metrics_controller.go new file mode 100644 index 0000000..1d9c8e5 --- /dev/null +++ b/controllers/metrics_controller.go @@ -0,0 +1,32 @@ +package controllers + +import ( + "InfantrySkillCalculator/models" + "InfantrySkillCalculator/utils" + "encoding/json" + "io" + "log" + "os" +) + +func LoadMetrics() { + f, err := os.Open("./config/metrics.json") + if err != nil { + log.Fatal("Failed to open metrics.json: ", err) + } + defer func(f *os.File) { + _ = f.Close() + }(f) + + data, err := io.ReadAll(f) + if err != nil { + log.Fatal("Failed to read metrics.json: ", err) + } + + var metrics models.GameMetrics + if err := json.Unmarshal(data, &metrics); err != nil { + log.Fatal("Failed to deserialize metrics.json: ", err) + } + + utils.GameMetrics = metrics +} diff --git a/controllers/player_controller.go b/controllers/player_controller.go index 8946346..dc41a0c 100644 --- a/controllers/player_controller.go +++ b/controllers/player_controller.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "gorm.io/gorm" "gorm.io/gorm/clause" + "internal/cache" "log" "net/http" "os" @@ -37,24 +38,36 @@ func GetPlayersByClanHTML(c *gin.Context) { if err := models.DB. Where("clan_id = ?", utils.StringToUint(clanId)). Find(&players).Error; err != nil { + + c.String(http.StatusBadRequest, "") + log.Fatal(err) return } file, err := os.ReadFile("./templates/player_list_item.html") if err != nil { + c.String(http.StatusBadRequest, "") + log.Fatal(err) return } playerItem := string(file) + game, err := GetActiveGame(c) + if err != nil { + c.String(http.StatusBadRequest, "") + log.Fatal(err) + return + } + var htmlOptions string for _, player := range players { var score string - if val, err := GetCacheByPlayerIDGorm(player.ID, 0); err != nil { - score = "----" + if val, err := cache.GetScore(player.ID, game.Tag); err != nil || val == -1.0 { + score = "" } else { - score = utils.FloatToString(val) + score = fmt.Sprintf("%.2f", val) } - htmlOptions += fmt.Sprintf(playerItem, player.Name, score, player.ID, player.ID) + htmlOptions += fmt.Sprintf(playerItem, player.Name, score, utils.UintToString(player.ID)+"?game_tag="+game.Tag, player.ID, player.ID) } c.Header("Content-Type", "text/html") diff --git a/controllers/user_settings_controller.go b/controllers/user_settings_controller.go index f9e7283..16ba0a5 100644 --- a/controllers/user_settings_controller.go +++ b/controllers/user_settings_controller.go @@ -2,9 +2,10 @@ package controllers import ( "InfantrySkillCalculator/models" - "InfantrySkillCalculator/utils" + "errors" "github.com/gin-gonic/gin" "net/http" + "session" ) type UpdateSettingsInput struct { @@ -18,9 +19,11 @@ type UpdateSettingsInput struct { func GetSettings(c *gin.Context) { var settings models.UserSettings - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - var username string - username, _ = session.Values["username"].(string) + username, ok := session.GetUsername(c) + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"}) + return + } if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"}) @@ -37,13 +40,39 @@ func GetSettings(c *gin.Context) { c.JSON(http.StatusOK, sanitizedSettings) } +func GetActiveGame(c *gin.Context) (models.Game, error) { + var settings models.UserSettings + var game models.Game + username, ok := session.GetUsername(c) + + if !ok { + err := errors.New("not logged in") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return models.Game{}, err + } + + if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"}) + return models.Game{}, err + } + + if err := models.DB.Where("id = ?", settings.ActiveGameID).First(&game).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "No active game available!"}) + return models.Game{}, err + } + + return game, nil +} + // UpdateSettings PATCH /settings func UpdateSettings(c *gin.Context) { var settings models.UserSettings - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - var username string - username, _ = session.Values["username"].(string) + username, ok := session.GetUsername(c) + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"}) + return + } if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"}) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6b3e52d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' + +services: + isc: + image: docker.maximilian.link/isc:dev + ports: + - "127.0.0.1:8000:8000" + environment: + - GO_ENV=production + - REDIS_ADDRESS=redis + depends_on: + - redis + volumes: + - ./app:/app + redis: + image: redis:alpine + ports: + - "127.0.0.1:6379:6379" \ No newline at end of file diff --git a/go.mod b/go.mod index d6a9aee..1b43980 100644 --- a/go.mod +++ b/go.mod @@ -7,21 +7,34 @@ require ( github.com/gorilla/sessions v1.2.2 github.com/redis/go-redis/v9 v9.4.0 golang.org/x/crypto v0.9.0 - gorm.io/driver/sqlite v1.5.4 gorm.io/gorm v1.25.5 ) +require internal/cache v1.0.0 + +replace internal/cache => ./internal/cache + +require ( + github.com/glebarez/sqlite v1.10.0 + internal/session v1.0.0 +) + +replace internal/session => ./internal/session + require ( github.com/bytedance/sonic v1.9.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -29,10 +42,10 @@ require ( github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect @@ -41,4 +54,8 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) diff --git a/go.sum b/go.sum index 6ef2ffc..7edf668 100644 --- a/go.sum +++ b/go.sum @@ -15,12 +15,18 @@ 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc= +github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -37,6 +43,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= @@ -54,8 +64,6 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -67,6 +75,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk= github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -95,8 +106,9 @@ golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -105,8 +117,14 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= -gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/go.work b/go.work new file mode 100644 index 0000000..0f4511e --- /dev/null +++ b/go.work @@ -0,0 +1,7 @@ +go 1.21 + +use ( + . + internal/cache + internal/session +) \ No newline at end of file diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..f6f822d --- /dev/null +++ b/go.work.sum @@ -0,0 +1,31 @@ +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..6db81bd --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,55 @@ +package cache + +import ( + "InfantrySkillCalculator/models" + "InfantrySkillCalculator/utils" + "context" + "errors" + "fmt" + "github.com/redis/go-redis/v9" +) + +var ctx = context.Background() + +func GetValue(key string) (string, error) { + val, err := models.Cache.Get(ctx, key).Result() + if err != nil { + return "", err // cache miss or error + } else { + return val, nil // cache hit + } +} + +func SetValue(key string, value interface{}) error { + return models.Cache.Set(ctx, key, value, utils.PlayerCacheLifetime).Err() +} + +func SetScore(playerId uint, gameTag string, score float32) error { + key := GetPlayerCacheKey(playerId, gameTag) + return SetValue(key, score) +} + +func GetScore(playerId uint, gameTag string) (float32, error) { + key := GetPlayerCacheKey(playerId, gameTag) + val, err := GetValue(key) + if errors.Is(err, redis.Nil) { + return -1.0, nil // cache miss + } else if err != nil { + return -1.0, err // cache error + } else { + return utils.StringToFloat(val), nil // cache hit + } +} + +func DeleteScore(playerId uint, gameTag string) error { + key := GetPlayerCacheKey(playerId, gameTag) + return models.Cache.Del(ctx, key).Err() +} + +func PurgeCache() error { + return models.Cache.FlushAll(ctx).Err() +} + +func GetPlayerCacheKey(playerId uint, gameTag string) string { + return fmt.Sprintf("player:%d:game:%s", playerId, gameTag) +} diff --git a/internal/cache/go.mod b/internal/cache/go.mod new file mode 100644 index 0000000..068cf29 --- /dev/null +++ b/internal/cache/go.mod @@ -0,0 +1,3 @@ +module cache + +go 1.21 \ No newline at end of file diff --git a/internal/session/go.mod b/internal/session/go.mod new file mode 100644 index 0000000..92492da --- /dev/null +++ b/internal/session/go.mod @@ -0,0 +1,3 @@ +module session + +go 1.21 \ No newline at end of file diff --git a/internal/session/session.go b/internal/session/session.go new file mode 100644 index 0000000..75e8b34 --- /dev/null +++ b/internal/session/session.go @@ -0,0 +1,43 @@ +package session + +import ( + "github.com/gin-gonic/gin" + "github.com/gorilla/sessions" +) + +var store = sessions.NewCookieStore([]byte("f0q0qew0!)§(ds9713lsda231")) + +const LoginSessionName = "session" + +func GetUsername(c *gin.Context) (string, bool) { + session, _ := getSession(c) + username, ok := session.Values["username"].(string) + return username, ok +} + +func GetAuthenticated(c *gin.Context) (bool, bool) { + session, _ := getSession(c) + auth, ok := session.Values["authenticated"].(bool) + return auth, ok +} + +func getSession(c *gin.Context) (*sessions.Session, error) { + return store.Get(c.Request, LoginSessionName) +} + +func InvalidateSession(c *gin.Context) error { + session, _ := getSession(c) + session.Options.MaxAge = -1 + err := session.Save(c.Request, c.Writer) + + return err +} + +func SetLoginSession(username string, c *gin.Context) error { + session, _ := getSession(c) + session.Values["authenticated"] = true + session.Values["username"] = username + err := session.Save(c.Request, c.Writer) + + return err +} diff --git a/main.go b/main.go index 0e66da0..0347382 100644 --- a/main.go +++ b/main.go @@ -53,10 +53,14 @@ func init() { if err != nil { log.Fatal(err) } + + controllers.LoadMetrics() } func main() { - //gin.SetMode(gin.ReleaseMode) // uncomment upon release + if os.Getenv("GO_ENV") == "production" { + gin.SetMode(gin.ReleaseMode) + } router := gin.New() err := router.SetTrustedProxies([]string{"127.0.0.1"}) @@ -72,6 +76,13 @@ func main() { models.ConnectDatabase() models.ConnectCache() + var code models.ActivationCode + if err := models.DB.First(&code).Error; err != nil { + firstCode := utils.GenerateActivationCode() + models.DB.Create(&models.ActivationCode{Code: firstCode, UserRole: models.AdminRole}) + log.Println("Created first activation code with ADMIN role:\n" + firstCode) + } + f, _ := os.OpenFile("isc_rest.log", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0660) utils.GinWriter = io.MultiWriter(f, os.Stdout) router.Use( @@ -114,6 +125,8 @@ func main() { protected.GET("/cache/:player_id", controllers.GetCacheByPlayerID) + protected.GET("/score/:player_id", controllers.GetScoreByPlayerID) + protected.GET("/game", controllers.GetGames) protected.GET("/game_html", controllers.GetGamesHTML) diff --git a/models/cache.go b/models/cache.go deleted file mode 100644 index a00fa55..0000000 --- a/models/cache.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -import "time" - -type PlayerCache struct { - CacheID uint `json:"cache_id" gorm:"primary_key"` - PlayerID uint `json:"player_id"` - CacheDate time.Time `json:"date"` - Score float32 `json:"score" gorm:"default:-1.0"` - Game string `json:"game"` -} diff --git a/models/metric_setting.go b/models/metric_setting.go deleted file mode 100644 index 3ccd374..0000000 --- a/models/metric_setting.go +++ /dev/null @@ -1,9 +0,0 @@ -package models - -type MetricSetting struct { - ID uint `json:"id" gorm:"primary_key"` - Name string `json:"name" binding:"required"` - WeaponCategory string `json:"weapon_category" binding:"required"` - Value float64 `json:"value" binding:"required"` - Game string `json:"game" binding:"required"` -} diff --git a/models/metrics_json.go b/models/metrics_json.go new file mode 100644 index 0000000..b486cbd --- /dev/null +++ b/models/metrics_json.go @@ -0,0 +1,18 @@ +package models + +type GameMetrics struct { + GameMetrics []GameMetric +} + +type GameMetric struct { + NormalizeFactor float64 + TopWeaponCount int + GameName string + WeaponMetrics []WeaponMetric +} + +type WeaponMetric struct { + WeaponCategory string + AccuracyFactor float64 + KpmFactor float64 +} diff --git a/models/setup.go b/models/setup.go index 0ff3f48..3340fc7 100644 --- a/models/setup.go +++ b/models/setup.go @@ -1,13 +1,13 @@ package models import ( - "InfantrySkillCalculator/utils" "context" + "github.com/glebarez/sqlite" "github.com/redis/go-redis/v9" - "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "log" + "os" ) var DB *gorm.DB @@ -33,10 +33,6 @@ func ConnectDatabase() { if err != nil { log.Fatal(err) } - err = database.AutoMigrate(&PlayerCache{}) - if err != nil { - log.Fatal(err) - } err = database.AutoMigrate(&User{}) if err != nil { log.Fatal(err) @@ -44,13 +40,6 @@ func ConnectDatabase() { err = database.AutoMigrate(&ActivationCode{}) if err != nil { log.Fatal(err) - } else { - var code ActivationCode - if err := database.First(&code).Error; err != nil { - firstCode := utils.GenerateActivationCode() - database.Create(&ActivationCode{Code: firstCode, UserRole: AdminRole}) - log.Println("Created first activation code with ADMIN role:\n" + firstCode) - } } err = database.AutoMigrate(&Game{}) if err != nil { @@ -64,10 +53,6 @@ func ConnectDatabase() { } } - err = database.AutoMigrate(&MetricSetting{}) - if err != nil { - log.Fatal(err) - } err = database.AutoMigrate(&UserSettings{}) if err != nil { log.Fatal(err) @@ -77,8 +62,17 @@ func ConnectDatabase() { } func ConnectCache() { + address := os.Getenv("REDIS_ADDRESS") + if address == "" { + address = "127.0.0.1" + } + port := os.Getenv("REDIS_PORT") + if port == "" { + port = "6379" + } + Cache = redis.NewClient(&redis.Options{ - Addr: "127.0.0.1:6379", + Addr: address + ":" + port, Password: "", DB: 0, }) diff --git a/models/tracker_json.go b/models/tracker_json.go new file mode 100644 index 0000000..4e9ec9d --- /dev/null +++ b/models/tracker_json.go @@ -0,0 +1,16 @@ +package models + +type TrackerWeaponJSON struct { + Weapons []Weapon `json:"weapons"` +} + +type Weapon struct { + Type string `json:"type"` + Name string `json:"weaponName"` + ID string `json:"id"` + Kills int `json:"kills"` + KPM float64 `json:"killsPerMinute"` + ShotsFired int `json:"shotsFired"` + ShotsHit int `json:"shotsHit"` + Accuracy float64 `json:"-"` +} diff --git a/pages.go b/pages.go index 5a11653..60e10cc 100644 --- a/pages.go +++ b/pages.go @@ -2,10 +2,10 @@ package main import ( "InfantrySkillCalculator/controllers" - "InfantrySkillCalculator/utils" "github.com/gin-gonic/gin" "log" "net/http" + "session" ) func mainPage(c *gin.Context) { @@ -20,9 +20,7 @@ func mainPage(c *gin.Context) { } func loginPage(c *gin.Context) { - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - - if auth, ok := session.Values["authenticated"].(bool); ok && auth { + if auth, ok := session.GetAuthenticated(c); ok && auth { c.Redirect(http.StatusFound, "/") return } @@ -42,11 +40,7 @@ func loginPost(c *gin.Context) { return } - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - session.Values["authenticated"] = true - session.Values["username"] = username - err := session.Save(c.Request, c.Writer) - if err != nil { + if err := session.SetLoginSession(username, c); err != nil { c.JSON(http.StatusInternalServerError, nil) return } @@ -56,10 +50,7 @@ func loginPost(c *gin.Context) { } func logout(c *gin.Context) { - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - session.Values["authenticated"] = false - err := session.Save(c.Request, c.Writer) - if err != nil { + if err := session.InvalidateSession(c); err != nil { c.JSON(http.StatusInternalServerError, nil) return } @@ -68,9 +59,7 @@ func logout(c *gin.Context) { } func registerPage(c *gin.Context) { - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - - if auth, ok := session.Values["authenticated"].(bool); ok && auth { + if auth, ok := session.GetAuthenticated(c); ok && auth { c.Redirect(http.StatusFound, "/") return } @@ -99,11 +88,7 @@ func registerPost(c *gin.Context) { controllers.CreateUser(username, hashedPassword, true, code) - session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) - session.Values["authenticated"] = true - session.Values["username"] = username - err = session.Save(c.Request, c.Writer) - if err != nil { + if err := session.SetLoginSession(username, c); err != nil { c.JSON(http.StatusInternalServerError, nil) return } diff --git a/static/index.css b/static/index.css index d6c9eb0..7e014bc 100644 --- a/static/index.css +++ b/static/index.css @@ -51,5 +51,4 @@ body, html { .form-control.overflow-auto { height: auto; /* Adjust as needed */ max-height: 300px; /* Adjust as needed */ -} - +} \ No newline at end of file diff --git a/static/index.js b/static/index.js index d51792e..4c6bc8d 100644 --- a/static/index.js +++ b/static/index.js @@ -1,5 +1,5 @@ document.addEventListener('DOMContentLoaded', function() { - const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + const tooltipTriggerList = document.querySelectorAll('[config-bs-toggle="tooltip"]'); tooltipTriggerList.forEach((elem) => { new bootstrap.Tooltip(elem); }); @@ -180,3 +180,13 @@ function createCodeDialog(btn) { } }); } + +function singleCalcSpinner(sender) { + const spinner = ''; + const score = sender.previousElementSibling.children[1]; + score.innerHTML = spinner; + sender.disabled = true; + sender.addEventListener('htmx:afterRequest', function () { + sender.disabled = false; + }, {once: true}); +} \ No newline at end of file diff --git a/templates/components/bottom_controls.html b/templates/components/bottom_controls.html index 78f8d47..18ca1aa 100644 --- a/templates/components/bottom_controls.html +++ b/templates/components/bottom_controls.html @@ -61,13 +61,13 @@
-
- diff --git a/templates/modals/add_clan.html b/templates/modals/add_clan.html index 4eb1b1c..427e9d8 100644 --- a/templates/modals/add_clan.html +++ b/templates/modals/add_clan.html @@ -86,7 +86,7 @@ } function createSubmitClanHandler(modalEvent) { - return function submitClanHandler(e) { + return function submitClanHandler(_) { const [clanList, otherClanList] = getClanLists(modalEvent); if (!validateInputs()) { @@ -130,7 +130,7 @@ submitButton.addEventListener('click', submitClanHandler); }); - addClanModal.addEventListener('hidden.bs.modal', event => { + addClanModal.addEventListener('hidden.bs.modal', _ => { submitButton.removeEventListener('click', submitClanHandler); clanName.value = ""; diff --git a/templates/modals/add_player.html b/templates/modals/add_player.html index d7dc73d..bcced30 100644 --- a/templates/modals/add_player.html +++ b/templates/modals/add_player.html @@ -38,8 +38,8 @@ const playerName = addPlayerModal.querySelector('#playerName'); const clanName = addPlayerModal.querySelector('#playerClanName'); const errorDiv = addPlayerModal.querySelector('.error-message'); - const homeClanListIndex = document.getElementById('home-clan').selectedIndex; - const oppClanListIndex = document.getElementById('opponent-clan').selectedIndex; + const homeClanList = document.getElementById('home-clan'); + const oppClanList = document.getElementById('opponent-clan'); function validateInput() { if (playerName.value.length < 1) { @@ -50,8 +50,8 @@ return true; } - function createSubmitPlayerHandler(modalEvent) { - return function submitPlayerHandler(e) { + function createSubmitPlayerHandler(_) { + return function submitPlayerHandler(_) { if (!validateInput()) return; @@ -74,7 +74,7 @@ return response.text(); }) .then(() => { - const sameClan = homeClanListIndex === oppClanListIndex; + const sameClan = homeClanList.selectedIndex === oppClanList.selectedIndex; if (playerList.id === 'home-player-list' || sameClan) htmx.ajax('GET', '/players_html', {target: '#home-player-list', values: {"clan_id": getSelectedClanId("home-clan")}}); if (playerList.id === 'opponent-player-list' || sameClan) @@ -98,7 +98,7 @@ clanName.value = selectedClan.innerText; }); - addPlayerModal.addEventListener('hidden.bs.modal', event => { + addPlayerModal.addEventListener('hidden.bs.modal', _ => { submitButton.removeEventListener('click', submitPlayerHandler); playerName.value = ""; diff --git a/templates/modals/delete_clan.html b/templates/modals/delete_clan.html index db4d39c..fcc5ae0 100644 --- a/templates/modals/delete_clan.html +++ b/templates/modals/delete_clan.html @@ -24,6 +24,7 @@ document.addEventListener('DOMContentLoaded', function() { const deleteClanModal = document.getElementById('deleteClanModal') const deleteClanModalBS = new bootstrap.Modal('#deleteClanModal'); + if (deleteClanModal) { deleteClanModal.addEventListener('show.bs.modal', event => { const [clanList, otherClanList] = getClanLists(event); diff --git a/templates/modals/delete_player.html b/templates/modals/delete_player.html index a3fe526..bbfc67b 100644 --- a/templates/modals/delete_player.html +++ b/templates/modals/delete_player.html @@ -34,8 +34,8 @@ modalBodyInput.innerText = selectedPlayer; const playerListId = button.closest('ul').parentElement.parentElement.id; - const homeClanListIndex = document.getElementById('home-clan').selectedIndex; - const oppClanListIndex = document.getElementById('opponent-clan').selectedIndex; + const homeClanList = document.getElementById('home-clan'); + const oppClanList = document.getElementById('opponent-clan'); const submitButton = deletePlayerModal.querySelector('button[name="submit"]'); submitButton.addEventListener('click', function () { @@ -46,7 +46,7 @@ } }) .then(() => { - const sameClan = homeClanListIndex === oppClanListIndex; + const sameClan = homeClanList.selectedIndex === oppClanList.selectedIndex; if (playerListId === 'home-player-list' || sameClan) htmx.ajax('GET', '/players_html', {target: '#home-player-list', values: {"clan_id": getSelectedClanId("home-clan")}}); if (playerListId === 'opponent-player-list' || sameClan) diff --git a/templates/modals/edit_clan.html b/templates/modals/edit_clan.html index ab1b819..dbc584d 100644 --- a/templates/modals/edit_clan.html +++ b/templates/modals/edit_clan.html @@ -87,7 +87,7 @@ } function createSubmitClanHandler() { - return function submitClanHandler(e) { + return function submitClanHandler(_) { if (!validateInputs()) return; @@ -149,7 +149,7 @@ }); }); - editClanModal.addEventListener('hidden.bs.modal', event => { + editClanModal.addEventListener('hidden.bs.modal', _ => { submitButton.removeEventListener('click', submitClanHandler); clanName.value = ""; diff --git a/templates/modals/edit_player.html b/templates/modals/edit_player.html index 42a65b1..1633738 100644 --- a/templates/modals/edit_player.html +++ b/templates/modals/edit_player.html @@ -34,8 +34,8 @@ const submitButton = editPlayerModal.querySelector('button[name="submit"]'); const playerName = editPlayerModal.querySelector('#editPlayerName'); const errorDiv = editPlayerModal.querySelector('.error-message'); - const homeClanListIndex = document.getElementById('home-clan').selectedIndex; - const oppClanListIndex = document.getElementById('opponent-clan').selectedIndex; + const homeClanList = document.getElementById('home-clan'); + const oppClanList = document.getElementById('opponent-clan'); function validateInput() { if (playerName.value.length < 1) { @@ -71,7 +71,7 @@ return response.text(); }) .then(() => { - const sameClan = homeClanListIndex === oppClanListIndex; + const sameClan = homeClanList.selectedIndex === oppClanList.selectedIndex; if (playerList.id === 'home-player-list' || sameClan) htmx.ajax('GET', '/players_html', {target: '#home-player-list', values: {"clan_id": getSelectedClanId("home-clan")}}); if (playerList.id === 'opponent-player-list' || sameClan) @@ -95,7 +95,7 @@ playerName.value = event.relatedTarget.closest('.input-group').querySelector('span').innerText; }); - editPlayerModal.addEventListener('hide.bs.modal', event => { + editPlayerModal.addEventListener('hide.bs.modal', _ => { submitButton.removeEventListener('click', submitPlayerHandler); playerName.value = ""; diff --git a/templates/modals/settings.html b/templates/modals/settings.html index 534a451..e650efb 100644 --- a/templates/modals/settings.html +++ b/templates/modals/settings.html @@ -53,7 +53,7 @@ const useCache = document.getElementById('settingsUseCache'); function createSubmitSettingsHandler() { - return function submitSettingsHandler(e) { + return function submitSettingsHandler(_) { let activeGameId = games.options[games.selectedIndex].value; if (activeGameId === '') { alert('Bitte wähle ein Spiel aus.'); @@ -88,7 +88,7 @@ } if (settingsModal) { - settingsModal.addEventListener('show.bs.modal', event => { + settingsModal.addEventListener('show.bs.modal', _ => { submitSettingsHandler = createSubmitSettingsHandler(); submitButton.addEventListener('click', submitSettingsHandler); diff --git a/templates/player_list_item.html b/templates/player_list_item.html index 24264ee..688c4c6 100644 --- a/templates/player_list_item.html +++ b/templates/player_list_item.html @@ -3,10 +3,10 @@
%s - - %s - - diff --git a/utils/globals.go b/utils/globals.go index f9c11a9..0255223 100644 --- a/utils/globals.go +++ b/utils/globals.go @@ -1,11 +1,11 @@ package utils import ( + "InfantrySkillCalculator/models" "io" "time" ) -var ClanLastChanged time.Time = time.Now().UTC() -var CacheLastChanged time.Time = time.Now().UTC() -var PlayersLastChanged time.Time = time.Now().UTC() var GinWriter io.Writer = nil +var GameMetrics models.GameMetrics +var PlayerCacheLifetime = 24 * time.Hour diff --git a/utils/score.go b/utils/score.go new file mode 100644 index 0000000..2863a8f --- /dev/null +++ b/utils/score.go @@ -0,0 +1,129 @@ +package utils + +import ( + "InfantrySkillCalculator/models" + "encoding/json" + "fmt" + "io" + "log" + "math" + "net/http" + "sort" +) + +func GetGameMetric(gameTag string) *models.GameMetric { + for _, metric := range GameMetrics.GameMetrics { + if metric.GameName == gameTag { + return &metric + } + } + return nil +} + +func FindWeaponMetric(weaponMetrics []models.WeaponMetric, weaponCategory string) *models.WeaponMetric { + for _, metric := range weaponMetrics { + if metric.WeaponCategory == weaponCategory { + return &metric + } + } + return nil +} + +func CalcPlayerScore(playerName string, gameTag string) float32 { + if gameTag != "BF5" && gameTag != "BF2042" { + _, _ = fmt.Fprintf(GinWriter, "UNSUPPORTED GAME: "+gameTag+"\n") + return -1 + } + + gameMetrics := GetGameMetric(gameTag) + if gameMetrics == nil { + _, _ = fmt.Fprintf(GinWriter, "No game metrics specified for '"+gameTag+"'\n") + return -1 + } + + normalizeFactor := gameMetrics.NormalizeFactor + topWeaponCount := gameMetrics.TopWeaponCount + + c := http.Client{} + + var reqUri string + if gameTag == "BF5" { + reqUri = "https://api.gametools.network/bfv/weapons/?raw=false&format_values=false&name=" + playerName + "&platform=pc" + } else if gameTag == "BF2042" { + reqUri = "https://api.gametools.network/bf2042/stats/?raw=false&format_values=false&name=" + playerName + "&platform=pc" + } + + req, err := http.NewRequest("GET", reqUri, nil) + if err != nil { + _, _ = fmt.Fprintf(GinWriter, err.Error()+"\n") + return -1 + } + + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36") + + res, err := c.Do(req) + if err != nil { + _, _ = fmt.Fprintf(GinWriter, err.Error()+"\n") + return -1 + } + + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) + if res.StatusCode == 404 { + _, _ = fmt.Fprintf(GinWriter, "User '"+playerName+"' does not exist!\n") + return -1 + } else if res.StatusCode != 200 { + log.Fatalf("Status code error: %d %s", res.StatusCode, res.Status) + } + + var data models.TrackerWeaponJSON + var body, _ = io.ReadAll(res.Body) + if err := json.Unmarshal(body, &data); err != nil { + log.Fatalf("Failed to deserialize tracker API response: %s", err) + } + + sort.SliceStable(data.Weapons, func(i, j int) bool { return data.Weapons[i].Kills > data.Weapons[j].Kills }) + var top []models.Weapon + + for _, weapon := range data.Weapons { + if len(top) >= topWeaponCount { + break + } + if gameTag == "BF2042" && weapon.Kills < 100 { + break + } + + acc := (float64(weapon.ShotsHit) / float64(weapon.ShotsFired)) * 100 + kpm := weapon.KPM + + weaponMetrics := FindWeaponMetric(gameMetrics.WeaponMetrics, weapon.Type) + if weaponMetrics == nil { + _, _ = fmt.Fprintf(GinWriter, "No weapon metrics specified for '"+gameTag+"', WType '"+weapon.Type+"'\n") + continue + } + + accFactor := weaponMetrics.AccuracyFactor + kpmFactor := weaponMetrics.KpmFactor + + acc /= accFactor + kpm /= kpmFactor + weapon.Accuracy = acc + weapon.KPM = kpm + + top = append(top, weapon) + } + + sumAcc := 0.0 + sumKpm := 0.0 + + for _, w := range top { + sumAcc += w.Accuracy + sumKpm += w.KPM + } + accAvg := sumAcc / float64(len(top)) + kpmAvg := sumKpm / float64(len(top)) + score := math.Round(((accAvg*kpmAvg)/normalizeFactor)*100) / 100 + + return float32(score) +} diff --git a/utils/session.go b/utils/session.go deleted file mode 100644 index e71c00a..0000000 --- a/utils/session.go +++ /dev/null @@ -1,7 +0,0 @@ -package utils - -import "github.com/gorilla/sessions" - -var Store = sessions.NewCookieStore([]byte("f0q0qew0!)§(ds9713lsda231")) - -const LoginSessionName = "session"