Compare commits

...

45 Commits

Author SHA1 Message Date
MaxJa4
f8d8ab968e Update go version in Dockerfile
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 52s
2025-02-09 17:06:16 +01:00
MaxJa4
98ccc8338c Show tooltip sooner
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 17s
2025-02-09 16:46:28 +01:00
MaxJa4
f97c5af26f Bump reqs 2025-02-09 16:46:20 +01:00
MaxJa4
e8dbf9611e Fix response decompression 2025-02-09 16:46:02 +01:00
MaxJa4
770a033429 Update go version and req-bump 2025-02-09 16:45:07 +01:00
MaxJa4
1d657f351c Disable retry and set timeout to 14s for CF timeout of 15s. Add accept-encoding for compression.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 9s
2024-02-11 22:15:10 +01:00
MaxJa4
bf8b1af964 Add timeout retry (once). Fix add-player modal issue.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 47s
2024-01-27 21:48:21 +01:00
MaxJa4
a4b341f421 Minor html optimizations. 2024-01-27 14:18:47 +01:00
MaxJa4
18d7d73005 Optimizations for screenshot download.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 41s
2024-01-27 14:10:48 +01:00
MaxJa4
0f2a11af43 Add max length for PW
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 51s
2024-01-27 13:57:39 +01:00
MaxJa4
866ff65eac Add tooltip delay and fix stuck tooltips. 2024-01-27 13:56:34 +01:00
MaxJa4
745b44eb10 Fix diff coloring.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 46s
2024-01-25 18:27:21 +01:00
MaxJa4
efd5c2007e Reload other list if same clan selected while quick-calc.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 49s
2024-01-25 18:12:55 +01:00
MaxJa4
93908f7da5 Adjusted full-calc statistics. Use new infantry skill algorithm.
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 7s
2024-01-25 18:04:45 +01:00
MaxJa4
7338d34765 Bugfixes and tooltips for full-calc ui.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 49s
2024-01-25 15:36:19 +01:00
MaxJa4
acf756765b Use bootstrap-modal for single-calc. 2024-01-25 15:18:12 +01:00
MaxJa4
130fe2ad57 UI optimizations and polish.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 46s
2024-01-24 12:01:23 +01:00
MaxJa4
0b2d10e7b7 Enhanced icons and tooltips.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 42s
2024-01-24 10:48:28 +01:00
MaxJa4
a0a469bc91 Fix docker action issue
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 48s
2024-01-24 09:40:32 +01:00
MaxJa4
6cfe8e3136 Fix docker action issue
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2024-01-24 09:36:13 +01:00
MaxJa4
165ddf8b76 Fix secrets
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 5s
2024-01-24 09:31:18 +01:00
MaxJa4
4cc4ce4736 Update actions and fix link
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 23s
2024-01-24 09:29:15 +01:00
MaxJa4
708620489d Create build + push action.
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 8s
2024-01-24 09:26:31 +01:00
MaxJa4
7e337cbaa7 Add demo action
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 24s
2024-01-23 19:06:46 +01:00
MaxJa4
2759a0a525 Add burst_queue (for now just to archive it) 2024-01-23 18:41:00 +01:00
MaxJa4
fa5728fba2 Remove game tags and persona/nucleus ids everywhere. Forward score-fetch statuscode. Fix login issue. 2024-01-23 18:40:39 +01:00
MaxJa4
2139b83174 Enable Enter-Press for submit in modals. 2024-01-23 09:33:56 +01:00
MaxJa4
1a7d6cbe11 Fix player deletion. Add PersonaID + NucleusID fetching upon Add-Player action. 2024-01-22 22:12:30 +01:00
MaxJa4
973ca5bb78 Fix player deletion. Add PersonaID + NucleusID fetching upon Add-Player action. 2024-01-22 22:10:29 +01:00
MaxJa4
37f9396428 Support reverse proxy IP logging. 2024-01-22 18:07:21 +01:00
MaxJa4
ef3117ea2a Ignore .git folder in docker img. Use rotating file logger. 2024-01-22 17:31:04 +01:00
MaxJa4
ca697da0da Entire logging rework. Full error handling. Small improvements. 2024-01-22 17:24:17 +01:00
MaxJa4
da1ff4e4e5 Implement KeepUpdated. Switch to Logrus logger. Relocate score-related functions. 2024-01-22 16:03:18 +01:00
MaxJa4
7cdc18bd78 Disable settings completely, BF2042 is default game. 2024-01-22 15:51:56 +01:00
MaxJa4
6441aa9f81 Add full-calc 2024-01-22 12:42:11 +01:00
MaxJa4
938e4ef348 Make modals static. Hide some settings. Minor optimizations. Switch from NPM bach to CDN. 2024-01-22 12:41:47 +01:00
MaxJa4
14b4856d47 Fix packages. 2024-01-21 19:00:34 +01:00
MaxJa4
f8a472f10c Fix refactor issues. 2024-01-21 18:59:11 +01:00
MaxJa4
a912c68450 Add persistence for redis to docker-compose 2024-01-21 18:06:27 +01:00
MaxJa4
f2573f2273 Player cache and project structure refactor. 2024-01-21 18:02:27 +01:00
MaxJa4
4aae0896aa Optimizations. User-Role handling in templates and routes. 2024-01-21 17:24:29 +01:00
MaxJa4
8edbbb4347 Add Single-Calc for player name. Refactor admin+single-calc dialogs. 2024-01-21 12:12:47 +01:00
MaxJa4
4ff139b217 Bugfixes. Optimizations/refactor. Add redis for player-cache. Add docker files. Replace sqlite dep. Single-Calc for existing players. Game-Metrics in JSON. 2024-01-21 00:53:42 +01:00
MaxJa4
4f9de31cce Merge remote-tracking branch 'origin/master' 2024-01-21 00:53:27 +01:00
MaxJa4
16d782fbe8 Bugfixes. Optimizations/refactor. Add redis for player-cache. Add docker files. Replace sqlite dep. Single-Calc for existing players. Game-Metrics in JSON. 2024-01-21 00:53:21 +01:00
64 changed files with 1968 additions and 802 deletions

View File

@@ -1,6 +1,8 @@
.db .git
.log *.db
*.log
redis redis
Dockerfile Dockerfile
docker-compose.yml docker-compose.yml
.gitignore .gitignore
.gitea

View File

@@ -0,0 +1,28 @@
name: Build and Push Docker Image
run-name: Docker image is being constructed 🚀
on:
push:
branches:
- master
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Log in to Gitea Docker Registry
uses: docker/login-action@v3
with:
registry: gitea.maximilian.link
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: gitea.maximilian.link/maxi/isc:dev

2
.idea/.gitignore generated vendored
View File

@@ -6,3 +6,5 @@
# Datasource local storage ignored files # Datasource local storage ignored files
/dataSources/ /dataSources/
/dataSources.local.xml /dataSources.local.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions

View File

@@ -5,9 +5,11 @@
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="bootstrap" level="application" />
<orderEntry type="library" name="bootstrap-icons" level="application" />
<orderEntry type="library" name="sweetalert2" level="application" /> <orderEntry type="library" name="sweetalert2" level="application" />
<orderEntry type="library" name="@sweetalert2/theme-dark" level="application" /> <orderEntry type="library" name="@sweetalert2/theme-dark" level="application" />
<orderEntry type="library" name="htmx.org" level="application" />
<orderEntry type="library" name="bootstrap" level="application" />
<orderEntry type="library" name="bootstrap-icons" level="application" />
<orderEntry type="library" name="html2canvas" level="application" />
</component> </component>
</module> </module>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="JavaScriptLibraryMappings"> <component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{@sweetalert2/theme-dark, bootstrap, bootstrap-icons, sweetalert2}" /> <includedPredefinedLibrary name="Node.js Core" />
</component> </component>
</project> </project>

View File

@@ -0,0 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Dockerfile-dev" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="isc:dev" />
<option name="buildCliOptions" value="--platform linux/arm64" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@@ -3,7 +3,7 @@
<deployment type="docker-image"> <deployment type="docker-image">
<settings> <settings>
<option name="imageTag" value="redis:alpine" /> <option name="imageTag" value="redis:alpine" />
<option name="command" value="redis-server --save 5 1 --loglevel warning" /> <option name="command" value="redis-server --save 5 1 --loglevel warning --notify-keyspace-events Ex" />
<option name="containerName" value="redis-isc" /> <option name="containerName" value="redis-isc" />
<option name="portBindings"> <option name="portBindings">
<list> <list>

View File

@@ -5,9 +5,9 @@
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="InfantrySkillCalculator" /> <package value="InfantrySkillCalculator" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/main.go" /> <filePath value="$PROJECT_DIR$/cmd/main.go" />
<method v="2"> <method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Redis" run_configuration_type="docker-deploy" /> <option name="RunConfigurationTask" enabled="false" run_configuration_name="Redis" run_configuration_type="docker-deploy" />
</method> </method>
</configuration> </configuration>
</component> </component>

15
.idea/webResources.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$/static" />
<path value="file://$PROJECT_DIR$/templates" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

View File

@@ -1,14 +1,10 @@
FROM golang:1.21-alpine AS builder FROM golang:1.23-alpine AS builder
WORKDIR /app WORKDIR /app
COPY go.mod go.sum internal/**/go.mod ./
RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o isc . RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -v -o isc .
FROM alpine:latest FROM alpine:latest

30
auth.go
View File

@@ -3,13 +3,13 @@ package main
import ( import (
"InfantrySkillCalculator/controllers" "InfantrySkillCalculator/controllers"
"InfantrySkillCalculator/models" "InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"errors" "errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
"internal/session" "internal/session"
"log"
"net/http" "net/http"
) )
@@ -30,7 +30,7 @@ func getUserPassword(username string) (string, error) {
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil { if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) { if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Fatal(err) utils.Logger.Fatalf("[AUTH] Failed to get password for user %s: %s", username, err)
} }
return "", err return "", err
} }
@@ -46,7 +46,7 @@ func hashPassword(password string) (string, error) {
return string(hashedPassword), nil return string(hashedPassword), nil
} }
func AuthRequired() gin.HandlerFunc { func ReaderAuthRequired() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
auth, okAuth := session.GetAuthenticated(c) auth, okAuth := session.GetAuthenticated(c)
username, okUser := session.GetUsername(c) username, okUser := session.GetUsername(c)
@@ -60,6 +60,20 @@ func AuthRequired() gin.HandlerFunc {
} }
} }
func AuthorAuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
auth, okAuth := session.GetAuthenticated(c)
username, okUser := session.GetUsername(c)
if !okAuth || !okUser || !auth || !controllers.IsUserEnabled(username) || controllers.GetUserRole(username) == models.ReaderRole {
redirectToLogin(c)
return
}
c.Next()
}
}
func AdminAuthRequired() gin.HandlerFunc { func AdminAuthRequired() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
auth, okAuth := session.GetAuthenticated(c) auth, okAuth := session.GetAuthenticated(c)
@@ -74,17 +88,9 @@ func AdminAuthRequired() gin.HandlerFunc {
} }
} }
func isUserAdmin(c *gin.Context) bool {
username, ok := session.GetUsername(c)
if !ok {
return false
}
return controllers.IsUserAdmin(username)
}
func redirectToLogin(c *gin.Context) { func redirectToLogin(c *gin.Context) {
if err := session.InvalidateSession(c); err != nil { if err := session.InvalidateSession(c); err != nil {
log.Fatal(err) utils.Logger.Fatalf("[AUTH] Failed to invalidate session: %s", err)
} }
c.Redirect(http.StatusFound, "/login") c.Redirect(http.StatusFound, "/login")
c.Abort() c.Abort()

View File

@@ -3,10 +3,7 @@ package controllers
import ( import (
"InfantrySkillCalculator/models" "InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils" "InfantrySkillCalculator/utils"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"internal/cache"
"log"
"net/http" "net/http"
) )
@@ -20,14 +17,14 @@ type UpdateCacheInput struct {
Score float32 `json:"score" gorm:"default:-1.0"` Score float32 `json:"score" gorm:"default:-1.0"`
} }
// GetCacheByPlayerID GET /cache/:player_id?game_tag=TAG // GetCacheByPlayerID GET /cache/:player_id
func GetCacheByPlayerID(c *gin.Context) { func GetCacheByPlayerID(c *gin.Context) {
playerId := utils.StringToUint(c.Param("player_id")) playerId := utils.StringToUint(c.Param("player_id"))
gameTag := c.Request.URL.Query().Get("game_tag")
val, err := cache.GetScore(playerId, gameTag) val, err := models.PlayerCache.GetScore(playerId)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Warnf("[CACHE] Record not found! Error: %s", err.Error())
} else { } else {
c.JSON(http.StatusOK, val) c.JSON(http.StatusOK, val)
} }
@@ -36,47 +33,51 @@ func GetCacheByPlayerID(c *gin.Context) {
// AddCache POST /cache // AddCache POST /cache
func AddCache(c *gin.Context) { func AddCache(c *gin.Context) {
var input AddCacheInput var input AddCacheInput
var game models.Game
if err := c.BindJSON(&input); err != nil { if err := c.BindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Warnf("[CACHE] Failed to bind JSON! Error: %s", err.Error())
return return
} }
var game models.Game if err := models.DB.Where("tag = ?", input.GameTag).First(&game).Error; err != nil {
if err := FindGameByTag(&game, input.GameTag).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Game not found!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Game not found!"})
utils.Logger.Warnf("[CACHE] Game not found! Error: %s", err.Error())
return return
} }
err := cache.SetScore(input.PlayerID, input.GameTag, input.Score) err := models.PlayerCache.SetScore(input.PlayerID, input.Score)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()})
utils.Logger.Warnf("[CACHE] Cache update failed! Error: %s", err.Error())
return return
} }
c.JSON(http.StatusOK, nil) c.JSON(http.StatusOK, nil)
} }
// UpdateCacheByPlayerID PATCH /cache/:id?game=TAG // UpdateCacheByPlayerID PATCH /cache/:id
func UpdateCacheByPlayerID(c *gin.Context) { func UpdateCacheByPlayerID(c *gin.Context) {
playerID := utils.StringToUint(c.Param("id")) playerID := utils.StringToUint(c.Param("id"))
gameTag := c.Request.URL.Query().Get("game")
score := utils.StringToFloat(c.PostForm("score")) score := utils.StringToFloat(c.PostForm("score"))
err := cache.SetScore(playerID, gameTag, score) err := models.PlayerCache.SetScore(playerID, score)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()})
utils.Logger.Warnf("[CACHE] Cache update failed! Error: %s", err.Error())
} else { } else {
c.JSON(http.StatusOK, nil) c.JSON(http.StatusOK, nil)
} }
} }
// DeleteCacheByPlayerID DELETE /cache/:id?game=TAG // DeleteCacheByPlayerID DELETE /cache/:id
func DeleteCacheByPlayerID(c *gin.Context) { func DeleteCacheByPlayerID(c *gin.Context) {
playerID := utils.StringToUint(c.Param("id")) playerID := utils.StringToUint(c.Param("id"))
gameTag := c.Request.URL.Query().Get("game")
if err := cache.DeleteScore(playerID, gameTag); err != nil { if err := models.PlayerCache.DeleteScore(playerID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Warnf("[CACHE] Cache deletion failed! Error: %s", err.Error())
} else { } else {
c.JSON(http.StatusOK, nil) c.JSON(http.StatusOK, nil)
} }
@@ -84,42 +85,42 @@ func DeleteCacheByPlayerID(c *gin.Context) {
// DeleteAllCaches DELETE /cache // DeleteAllCaches DELETE /cache
func DeleteAllCaches(c *gin.Context) { func DeleteAllCaches(c *gin.Context) {
if err := cache.PurgeCache(); err != nil { if err := models.PlayerCache.PurgeCache(); err != nil {
c.String(http.StatusBadRequest, err.Error()) c.String(http.StatusBadRequest, err.Error())
utils.Logger.Warnf("[CACHE] Cache purge failed! Error: %s", err.Error())
} else { } else {
c.String(http.StatusOK, "Purged all caches!") c.String(http.StatusOK, "Purged all caches!")
} }
} }
// GetScoreByPlayerID GET /score/:player_id?game_tag=TAG func UpdateCacheAfterExpiry(key string) {
func GetScoreByPlayerID(c *gin.Context) { utils.Logger.Infof("[KeepUpdated] Updating cache for key %s", key)
playerId, err := models.PlayerCache.GetPlayerIdFromCacheKey(key)
if err != nil {
utils.Logger.Fatal(err)
}
var player models.Player 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. if err := models.DB.
Model(&models.Player{}). Preload("Clan").
Where("id = ?", playerId). Where("id = ?", playerId).
First(&player).Error; err != nil { First(&player).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Player not found!"}) utils.Logger.Errorf("[KeepUpdated] Failed to find player with ID %d! Error: %s", playerId, err.Error())
return return
} }
score, err := cache.GetScore(player.ID, gameTag) if player.Clan.KeepUpdated {
if err != nil || score == -1 { score, statusCode := GetPlayerScoreNew(player.Name)
score = utils.CalcPlayerScore(player.Name, gameTag)
if score == score && score != -1 { // not NaN if score == score && score != -1 { // not NaN
if err := cache.SetScore(player.ID, gameTag, score); err != nil { if err := models.PlayerCache.SetScore(player.ID, score); err != nil {
log.Fatal(err) utils.Logger.Warnf("[KeepUpdated] Failed to update cache for player %s! Error: %s", player.Name, err.Error())
} else {
utils.Logger.Infof("[KeepUpdated] Updated cache for player %s", player.Name)
} }
} else {
utils.Logger.Warnf("[KeepUpdated] Failed to calculate score for player %s! Status code: %d", player.Name, statusCode)
} }
} }
if score != score || score == -1 { // NaN
c.String(http.StatusOK, "<i class=\"bi bi-person-x-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\"></i>")
} else if score != -1 {
c.String(http.StatusOK, fmt.Sprintf("%.2f", score))
} else {
c.JSON(http.StatusBadRequest, "Invalid request!")
}
} }

View File

@@ -7,7 +7,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
"log"
"net/http" "net/http"
) )
@@ -26,7 +25,11 @@ type UpdateClanInput struct {
// GetAllClans GET /clan // GetAllClans GET /clan
func GetAllClans(c *gin.Context) { func GetAllClans(c *gin.Context) {
var clans []models.Clan var clans []models.Clan
models.DB.Find(&clans) if err := models.DB.Find(&clans).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Errorf("[CLAN] Error while getting all clans: %s", err.Error())
return
}
c.JSON(http.StatusOK, clans) c.JSON(http.StatusOK, clans)
} }
@@ -34,7 +37,11 @@ func GetAllClans(c *gin.Context) {
// GetAllClansHTML GET /clans_html // GetAllClansHTML GET /clans_html
func GetAllClansHTML(c *gin.Context) { func GetAllClansHTML(c *gin.Context) {
var clans []models.Clan var clans []models.Clan
models.DB.Find(&clans) if err := models.DB.Find(&clans).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Errorf("[CLAN] Error while getting all clans: %s", err.Error())
return
}
var htmlOptions string var htmlOptions string
htmlOptions = `<option disabled selected value>Auswählen...</option>` htmlOptions = `<option disabled selected value>Auswählen...</option>`
@@ -49,37 +56,34 @@ func GetAllClansHTML(c *gin.Context) {
// AddClan POST /clan // AddClan POST /clan
func AddClan(c *gin.Context) { func AddClan(c *gin.Context) {
var input AddClanInput var input AddClanInput
var clan models.Clan
if err := c.BindJSON(&input); err != nil { if err := c.BindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return utils.Logger.Errorf("[CLAN] Error while binding JSON: %s", err.Error())
}
var clan models.Clan
if err := FindClanByName(&clan, input.Name).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Clan with this name already exists!"})
return
} else if err := FindClanByTag(&clan, input.Tag).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Clan with this tag already exists!"})
return return
} }
clan = models.Clan{Name: input.Name, Tag: input.Tag, KeepUpdated: input.KeepUpdated} clan = models.Clan{Name: input.Name, Tag: input.Tag, KeepUpdated: input.KeepUpdated}
models.DB.Create(&clan)
if err := models.DB.Create(&clan).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[CLAN] Error while adding clan '[%s] %s': %s", input.Tag, input.Name, err.Error())
return
}
c.JSON(http.StatusOK, clan) c.JSON(http.StatusOK, clan)
_, err := fmt.Fprintf(utils.GinWriter, "Added clan '"+clan.Name+"' with tag '"+clan.Tag+"'\n") utils.Logger.Infof("[CLAN] Added clan: [%s] %s", clan.Tag, clan.Name)
if err != nil {
log.Fatal(err)
}
} }
// GetClanByID GET /clan/:id // GetClanByID GET /clan/:id
func GetClanByID(c *gin.Context) { func GetClanByID(c *gin.Context) {
var clan models.Clan var clan models.Clan
if err := FindClanByID(&clan, c).Error; err != nil { if err := models.DB.Where("id = ?", c.Param("id")).First(&clan).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Errorf("[CLAN] Error while getting clan by ID: %s", err.Error())
return return
} }
@@ -89,47 +93,57 @@ func GetClanByID(c *gin.Context) {
// UpdateClanByID PATCH /clan/:id // UpdateClanByID PATCH /clan/:id
func UpdateClanByID(c *gin.Context) { func UpdateClanByID(c *gin.Context) {
var input UpdateClanInput var input UpdateClanInput
var clan models.Clan
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[CLAN] Error while binding JSON: %s", err.Error())
return return
} }
res := models.DB.Model(&models.Clan{}). if err := models.DB.First(&clan, c.Param("id")).Error; err != nil {
Where("id = ?", c.Param("id")). c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
Updates(map[string]interface{}{ utils.Logger.Errorf("[CLAN] Error while getting clan by ID: %s", err.Error())
"Name": input.Name, return
"Tag": input.Tag, }
"KeepUpdated": input.KeepUpdated,
}) oldValues := models.Clan{
if res.Error != nil { Name: clan.Name,
c.JSON(http.StatusBadRequest, gin.H{"error": res.Error.Error()}) Tag: clan.Tag,
KeepUpdated: clan.KeepUpdated,
}
clan.Name = input.Name
clan.Tag = input.Tag
clan.KeepUpdated = input.KeepUpdated
if err := models.DB.Save(&clan).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[CLAN] Error while updating clan '%s': %s", input.Name, err.Error())
return return
} }
c.JSON(http.StatusOK, nil) c.JSON(http.StatusOK, nil)
_, err := fmt.Fprintf(utils.GinWriter, "Updated clan '"+input.Name+"' with tag '"+input.Tag+"'\n") utils.Logger.Infof("[CLAN] Updated clan: [%s] %s -> [%s] %s", oldValues.Tag, oldValues.Name, clan.Tag, clan.Name)
if err != nil {
log.Fatal(err)
}
} }
// DeleteClanByID DELETE /clan/:id // DeleteClanByID DELETE /clan/:id
func DeleteClanByID(c *gin.Context) { func DeleteClanByID(c *gin.Context) {
var clan models.Clan var clan models.Clan
if err := FindClanByID(&clan, c).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) if err := models.DB.Clauses(clause.Returning{}).
Where("id = ?", c.Param("id")).
Delete(&clan).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[CLAN] Error while deleting clan '%s': %s", clan.Name, err.Error())
return return
} }
models.DB.Delete(&clan)
c.JSON(http.StatusOK, true) c.JSON(http.StatusOK, true)
_, err := fmt.Fprintf(utils.GinWriter, "Deleted clan '"+clan.Name+"' with tag '"+clan.Tag+"'\n") utils.Logger.Infof("[CLAN] Deleted clan: [%s] %s", clan.Tag, clan.Name)
if err != nil {
log.Fatal(err)
}
} }
// DeleteAllClans DELETE /admin/clan // DeleteAllClans DELETE /admin/clan
@@ -139,21 +153,12 @@ func DeleteAllClans(c *gin.Context) {
Session(&gorm.Session{AllowGlobalUpdate: true}). Session(&gorm.Session{AllowGlobalUpdate: true}).
Clauses(clause.Returning{}). Clauses(clause.Returning{}).
Delete(&clans).Error; err != nil { Delete(&clans).Error; err != nil {
c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error()) c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error())
utils.Logger.Errorf("[CLAN] Error while purging all clans: %s", err.Error())
return return
} }
c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(clans)))+" clans!") c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(clans)))+" clans!")
} utils.Logger.Infof("[CLAN] Purged %d clans!", len(clans))
func FindClanByName(out interface{}, name string) *gorm.DB {
return models.DB.Where("name = ?", name).First(out)
}
func FindClanByTag(out interface{}, tag string) *gorm.DB {
return models.DB.Where("tag = ?", tag).First(out)
}
func FindClanByID(out interface{}, c *gin.Context) *gorm.DB {
return models.DB.Where("id = ?", c.Param("id")).First(out)
} }

View File

@@ -5,6 +5,7 @@ import (
"InfantrySkillCalculator/utils" "InfantrySkillCalculator/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"strings"
) )
// CreateCode POST /code // CreateCode POST /code
@@ -12,6 +13,7 @@ func CreateCode(c *gin.Context) {
userRole, ok := c.GetPostForm("user_role") userRole, ok := c.GetPostForm("user_role")
if !ok { if !ok {
c.String(http.StatusBadRequest, "Missing user role") c.String(http.StatusBadRequest, "Missing user role")
utils.Logger.Error("Missing user role")
return return
} }
var role models.Role var role models.Role
@@ -24,14 +26,18 @@ func CreateCode(c *gin.Context) {
role = models.ReaderRole role = models.ReaderRole
default: default:
c.String(http.StatusInternalServerError, "Invalid user role: "+userRole) c.String(http.StatusInternalServerError, "Invalid user role: "+userRole)
utils.Logger.Error("Invalid user role: " + userRole)
return
} }
newCode := utils.GenerateActivationCode() newCode := utils.GenerateActivationCode()
newCodeObj := models.ActivationCode{Code: newCode, UserRole: role} newCodeObj := models.ActivationCode{Code: newCode, UserRole: role}
if err := models.DB.Create(&newCodeObj).Error; err != nil { if err := models.DB.Create(&newCodeObj).Error; err != nil {
c.String(http.StatusInternalServerError, "Failed to create new code: "+err.Error()) c.String(http.StatusInternalServerError, "Failed to create new code: "+err.Error())
utils.Logger.Error("Failed to create new code: " + err.Error())
return return
} }
c.String(http.StatusOK, "Activation code for role '"+string(role)+"': "+newCode) c.String(http.StatusOK, "Activation code for role '"+string(role)+"': "+newCode)
utils.Logger.Info("Activation code for role '" + string(role) + "' created: " + newCode[:4] + strings.Repeat("*", 28))
} }

View File

@@ -2,9 +2,10 @@ package controllers
import ( import (
"InfantrySkillCalculator/models" "InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm/clause"
"net/http" "net/http"
) )
@@ -21,7 +22,11 @@ type UpdateGameInput struct {
// GetGames GET /game // GetGames GET /game
func GetGames(c *gin.Context) { func GetGames(c *gin.Context) {
var games []models.Game var games []models.Game
models.DB.Find(&games) if err := models.DB.Find(&games).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Errorf("[GAME] Record not found: %v", err)
return
}
c.JSON(http.StatusOK, games) c.JSON(http.StatusOK, games)
} }
@@ -29,7 +34,11 @@ func GetGames(c *gin.Context) {
// GetGamesHTML GET /game_html // GetGamesHTML GET /game_html
func GetGamesHTML(c *gin.Context) { func GetGamesHTML(c *gin.Context) {
var games []models.Game var games []models.Game
models.DB.Find(&games) if err := models.DB.Find(&games).Error; err != nil {
c.String(http.StatusBadRequest, "")
utils.Logger.Errorf("[GAME] Record not found: %v", err)
return
}
var htmlOptions string var htmlOptions string
htmlOptions = `<option disabled selected value>Auswählen...</option>` htmlOptions = `<option disabled selected value>Auswählen...</option>`
@@ -46,6 +55,7 @@ func GetGameByID(c *gin.Context) {
var game models.Game var game models.Game
if err := models.DB.Where("id = ?", c.Param("id")).First(&game).Error; err != nil { if err := models.DB.Where("id = ?", c.Param("id")).First(&game).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Errorf("[GAME] Record not found: %v", err)
return return
} }
@@ -57,11 +67,16 @@ func AddGame(c *gin.Context) {
var input AddGameInput var input AddGameInput
if err := c.BindJSON(&input); err != nil { if err := c.BindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[GAME] Record not found: %v", err)
return return
} }
game := models.Game{Name: input.Name, Tag: input.Tag} game := models.Game{Name: input.Name, Tag: input.Tag}
models.DB.Create(&game) if err := models.DB.Create(&game).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[GAME] Record not found: %v", err)
return
}
c.JSON(http.StatusOK, game) c.JSON(http.StatusOK, game)
} }
@@ -71,12 +86,14 @@ func UpdateGameByID(c *gin.Context) {
var game models.Game var game models.Game
if err := models.DB.Where("id = ?", c.Param("id")).First(&game).Error; err != nil { if err := models.DB.Where("id = ?", c.Param("id")).First(&game).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Errorf("[GAME] Record not found: %v", err)
return return
} }
var input UpdateGameInput var input UpdateGameInput
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[GAME] Could not parse input: %v", err)
return return
} }
@@ -88,20 +105,13 @@ func UpdateGameByID(c *gin.Context) {
// DeleteGameByID DELETE /game/:id // DeleteGameByID DELETE /game/:id
func DeleteGameByID(c *gin.Context) { func DeleteGameByID(c *gin.Context) {
var game models.Game var game models.Game
if err := models.DB.Where("id = ?", c.Param("id")).First(&game).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) if err := models.DB.Clauses(clause.Returning{}).Delete(&game).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[GAME] Error while deleting game: %v", err)
return return
} }
models.DB.Delete(&game)
c.JSON(http.StatusOK, true) c.JSON(http.StatusOK, true)
} utils.Logger.Infof("[GAME] Deleted game: %s", game.Name)
func FindGame(out interface{}, c *gin.Context) *gorm.DB {
return models.DB.Where("id = ?", c.Param("id")).First(&out)
}
func FindGameByTag(out interface{}, tag string) *gorm.DB {
return models.DB.Where("tag = ?", tag).First(out)
} }

View File

@@ -5,14 +5,15 @@ import (
"InfantrySkillCalculator/utils" "InfantrySkillCalculator/utils"
"encoding/json" "encoding/json"
"io" "io"
"log"
"os" "os"
) )
var gameMetrics models.GameMetrics
func LoadMetrics() { func LoadMetrics() {
f, err := os.Open("./config/metrics.json") f, err := os.Open("./config/metrics.json")
if err != nil { if err != nil {
log.Fatal("Failed to open metrics.json: ", err) utils.Logger.Fatal("[METRICS] Failed to open metrics.json: ", err)
} }
defer func(f *os.File) { defer func(f *os.File) {
_ = f.Close() _ = f.Close()
@@ -20,13 +21,31 @@ func LoadMetrics() {
data, err := io.ReadAll(f) data, err := io.ReadAll(f)
if err != nil { if err != nil {
log.Fatal("Failed to read metrics.json: ", err) utils.Logger.Fatal("[METRICS] Failed to read metrics.json: ", err)
} }
var metrics models.GameMetrics var metrics models.GameMetrics
if err := json.Unmarshal(data, &metrics); err != nil { if err := json.Unmarshal(data, &metrics); err != nil {
log.Fatal("Failed to deserialize metrics.json: ", err) utils.Logger.Fatal("[METRICS] Failed to deserialize metrics.json: ", err)
} }
utils.GameMetrics = metrics gameMetrics = metrics
}
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
} }

View File

@@ -7,10 +7,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
"internal/cache" "html/template"
"log"
"net/http" "net/http"
"os"
) )
type AddPlayerInput struct { type AddPlayerInput struct {
@@ -26,7 +24,11 @@ type UpdatePlayerInput struct {
// GetAllPlayers GET /player // GetAllPlayers GET /player
func GetAllPlayers(c *gin.Context) { func GetAllPlayers(c *gin.Context) {
var players []models.Player var players []models.Player
models.DB.Find(&players) if err := models.DB.Find(&players).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Errorf("[PLAYER] Record not found! Error: %s", err.Error())
return
}
c.JSON(http.StatusOK, players) c.JSON(http.StatusOK, players)
} }
@@ -35,136 +37,143 @@ func GetAllPlayers(c *gin.Context) {
func GetPlayersByClanHTML(c *gin.Context) { func GetPlayersByClanHTML(c *gin.Context) {
var players []models.Player var players []models.Player
clanId := c.Request.URL.Query().Get("clan_id") clanId := c.Request.URL.Query().Get("clan_id")
if clanId == "NaN" {
c.String(http.StatusBadRequest, "")
return
}
if err := models.DB. if err := models.DB.
Where("clan_id = ?", utils.StringToUint(clanId)). Where("clan_id = ?", utils.StringToUint(clanId)).
Find(&players).Error; err != nil { Find(&players).Error; err != nil {
c.String(http.StatusBadRequest, "") c.String(http.StatusBadRequest, "")
log.Fatal(err) utils.Logger.Errorf("[PLAYER] Clan not found! Error: %s", err.Error())
return return
} }
file, err := os.ReadFile("./templates/player_list_item.html") var playerIDs []uint
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 { for _, player := range players {
var score string playerIDs = append(playerIDs, player.ID)
if val, err := cache.GetScore(player.ID, game.Tag); err != nil || val == -1.0 {
score = "<i class=\"bi bi-dash me-2\" style=\"margin-left:0.7rem;\"></i>"
} else {
score = fmt.Sprintf("%.2f", val)
}
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") scores, err := models.PlayerCache.GetScores(playerIDs)
c.String(http.StatusOK, htmlOptions) if err != nil {
c.String(http.StatusBadRequest, "")
utils.Logger.Errorf("[PLAYER] Could not get scores! Error: %s", err.Error())
return
}
userRole := GetUserRoleByCtx(c)
var data []map[string]interface{}
for i, player := range players {
score := scores[i]
var scoreStr string
if score == -1.0 {
scoreStr = "<i class=\"bi bi-dash me-2\" style=\"margin-left:0.7rem;\"></i>"
} else {
scoreStr = fmt.Sprintf("%.2f", score)
}
data = append(data, map[string]interface{}{
"PlayerName": player.Name,
"Score": template.HTML(scoreStr),
"PlayerID": player.ID,
"UserRole": userRole,
})
}
err = utils.PlayerItemTemplate.Execute(c.Writer, data)
if err != nil {
utils.Logger.Errorf("[PLAYER] Could not execute template! Error: %s", err.Error())
}
} }
// AddPlayer POST /player // AddPlayer POST /player
func AddPlayer(c *gin.Context) { func AddPlayer(c *gin.Context) {
var input AddPlayerInput var input AddPlayerInput
var player models.Player
if err := c.BindJSON(&input); err != nil { if err := c.BindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return utils.Logger.Errorf("[PLAYER] Could not bind JSON! Error: %s", err.Error())
}
var player models.Player
if err := models.DB.Where("name = ?", input.Name).First(&player).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Player with this name already exists!"})
return return
} }
player = models.Player{Name: input.Name, ClanID: input.ClanID} player = models.Player{Name: input.Name, ClanID: input.ClanID}
models.DB.Create(&player) if err := models.DB.Create(&player); err.Error != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error.Error()})
utils.Logger.Errorf("[PLAYER] Could not create player! Error: %s", err.Error.Error())
return
}
c.JSON(http.StatusOK, player) c.JSON(http.StatusOK, player)
_, err := fmt.Fprintf(utils.GinWriter, "Added player '"+player.Name+"'\n") utils.Logger.Infof("[PLAYER] Added player '%s' in clan %d", input.Name, input.ClanID)
if err != nil {
log.Fatal(err)
}
} }
// GetPlayerByID GET /player/:id // GetPlayerByID GET /player/:id
func GetPlayerByID(c *gin.Context) { func GetPlayerByID(c *gin.Context) {
player := FindPlayerByID(utils.StringToUint(c.Param("id"))) var player models.Player
if player == nil {
if err := models.DB.Where("id = ?", c.Param("id")).First(&player).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
utils.Logger.Errorf("[PLAYER] Could not find player! Error: %s", err.Error())
return return
} }
c.JSON(http.StatusOK, player) c.JSON(http.StatusOK, player)
} }
// GetPlayerIDByName GET /playerid/:name
func GetPlayerIDByName(c *gin.Context) {
var player models.Player
if err := FindPlayerByName(&player, c).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
c.JSON(http.StatusOK, player.ID)
}
// UpdatePlayerByID PATCH /player/:id // UpdatePlayerByID PATCH /player/:id
func UpdatePlayerByID(c *gin.Context) { func UpdatePlayerByID(c *gin.Context) {
var input UpdatePlayerInput var input UpdatePlayerInput
var player models.Player
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[PLAYER] Could not bind JSON! Error: %s", err.Error())
return return
} }
res := models.DB.Model(&models.Player{}). if err := models.DB.First(&player, c.Param("id")).Error; err != nil {
Where("id = ?", utils.StringToUint(c.Param("id"))). c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
Updates(map[string]interface{}{ utils.Logger.Errorf("[PLAYER] Could not find player! Error: %s", err.Error())
"Name": input.Name, return
"ClanID": input.ClanID, }
})
if res.Error != nil { oldValues := models.Player{
c.JSON(http.StatusBadRequest, gin.H{"error": res.Error.Error()}) Name: player.Name,
ClanID: player.ClanID,
}
player.Name = input.Name
player.ClanID = input.ClanID
if err := models.DB.Save(&player).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[PLAYER] Could not update player! Error: %s", err.Error())
return return
} }
c.JSON(http.StatusOK, nil) c.JSON(http.StatusOK, nil)
_, err := fmt.Fprintf(utils.GinWriter, "Updated player '"+input.Name+"'\n") utils.Logger.Infof("[PLAYER] Updated player: %s -> %s", oldValues.Name, player.Name)
if err != nil {
log.Fatal(err)
}
} }
// DeletePlayerByID DELETE /player/:id // DeletePlayerByID DELETE /player/:id
func DeletePlayerByID(c *gin.Context) { func DeletePlayerByID(c *gin.Context) {
player := FindPlayerByID(utils.StringToUint(c.Param("id"))) var player models.Player
if player == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) if err := models.DB.Clauses(clause.Returning{}).Where("id = ?", c.Param("id")).Delete(&player).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[PLAYER] Could not delete player! Error: %s", err.Error())
return return
} }
models.DB.Delete(&player)
c.JSON(http.StatusOK, true) c.JSON(http.StatusOK, true)
_, err := fmt.Fprintf(utils.GinWriter, "Deleted player '"+player.Name+"'\n") utils.Logger.Infof("[PLAYER] Deleted player: %s", player.Name)
if err != nil {
log.Fatal(err)
}
} }
// DeleteAllPlayers DELETE /admin/player // DeleteAllPlayers DELETE /admin/player
@@ -174,22 +183,12 @@ func DeleteAllPlayers(c *gin.Context) {
Session(&gorm.Session{AllowGlobalUpdate: true}). Session(&gorm.Session{AllowGlobalUpdate: true}).
Clauses(clause.Returning{}). Clauses(clause.Returning{}).
Delete(&players).Error; err != nil { Delete(&players).Error; err != nil {
c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error()) c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error())
utils.Logger.Errorf("[PLAYER] Could not purge players! Error: %s", err.Error())
return return
} }
c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(players)))+" players!") c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(players)))+" players!")
} utils.Logger.Infof("[PLAYER] Purged %d players!", len(players))
func FindPlayerByID(id uint) *models.Player {
var res models.Player
if err := models.DB.Where("id = ?", id).First(&res).Error; err != nil {
return nil
} else {
return &res
}
}
func FindPlayerByName(out interface{}, c *gin.Context) *gorm.DB {
return models.DB.Where("name = ?", c.Param("name")).First(out)
} }

View File

@@ -0,0 +1,223 @@
package controllers
import (
"InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net"
"net/http"
"sort"
"time"
"github.com/gin-gonic/gin"
)
// GetScoreByPlayerID GET /score/:player_id
func GetScoreByPlayerID(c *gin.Context) {
var player models.Player
if err := models.DB.Where("id = ?", c.Param("player_id")).First(&player).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Player not found!"})
utils.Logger.Errorf("[SCORE] Player not found: %s", err.Error())
return
}
score, err := models.PlayerCache.GetScore(player.ID)
var statusCode = http.StatusOK
if err != nil || score == -1 {
score, statusCode = GetPlayerScoreNew(player.Name)
if score == score && score != -1 { // not NaN
if err := models.PlayerCache.SetScore(player.ID, score); err != nil {
utils.Logger.Errorf("[SCORE] Failed to cache player score: %s", err.Error())
return
}
}
}
switch statusCode {
case 200:
c.String(200, fmt.Sprintf("%.2f", score))
case 404:
c.String(200, "<i class=\"bi bi-person-x-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\" data-bs-action=\"tooltip\" data-bs-title=\"Spieler nicht gefunden\"></i>")
case 503, 504, 408:
c.String(statusCode, "<i class=\"bi bi-cloud-slash-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\"data-bs-action=\"tooltip\" data-bs-title=\"Tracker nicht erreichbar\"></i>")
default:
c.String(statusCode, "<i class=\"bi bi-exclamation-circle-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\" data-bs-action=\"tooltip\" data-bs-title=\"Unbekannter Fehler\"></i>")
utils.Logger.Warnf("[SCORE] Invalid request! Player: %s, Score: %f", player.Name, score)
}
}
// GetScoreByPlayerName POST /score/:player_name
func GetScoreByPlayerName(c *gin.Context) {
playerName := c.Param("player_name")
score, statusCode := GetPlayerScoreNew(playerName)
switch statusCode {
case 200:
c.String(200, fmt.Sprintf("%.2f", score))
case 404:
c.String(200, "Spieler nicht gefunden!")
case 503, 504, 408:
c.String(statusCode, "Tracker nicht erreichbar!")
default:
c.String(statusCode, "Ungültige Abfrage!")
utils.Logger.Warnf("[SCORE] Invalid request! Player: %s, Score: %f", playerName, score)
}
}
func GetPlayerScore(playerName string) (float32, int) {
playerData, statusCode := getPlayerData(playerName)
if statusCode != 200 {
return -1, statusCode
}
return calcPlayerScore(playerData), statusCode
}
func GetPlayerScoreNew(playerName string) (float32, int) {
playerData, statusCode := getPlayerData(playerName)
//if statusCode == 408 { // retry once
// playerData, statusCode = getPlayerData(playerName)
//}
if statusCode != 200 {
return -1, statusCode
}
return calcPlayerScoreNew(playerData), statusCode
}
func getPlayerData(playerName string) (*models.TrackerDataJSON, int) {
c := http.Client{
Timeout: 14 * time.Second, // Cloudflare timeout is 15s
}
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 {
utils.Logger.Errorf("[SCORE] Failed to create request: %s", err.Error())
return nil, 0
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
res, err := c.Do(req)
if err != nil {
var e net.Error
if errors.As(err, &e) && e.Timeout() {
utils.Logger.Errorf("[SCORE] Request timeout!")
return nil, 408
}
utils.Logger.Errorf("[SCORE] Failed to send request: %s", err.Error())
return nil, 0
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(res.Body)
if res.StatusCode == 404 {
utils.Logger.Errorf("[SCORE] User does not exist!")
return nil, res.StatusCode
} else if res.StatusCode == 503 || res.StatusCode == 504 {
utils.Logger.Errorf("[SCORE] Service unavailable!")
return nil, res.StatusCode
} else if res.StatusCode != 200 {
utils.Logger.Errorf("[SCORE] Status code error: %d %s", res.StatusCode, res.Status)
return nil, res.StatusCode
}
encoding := res.Header.Get("Content-Encoding")
reader, err := utils.DecompressResponseBody(encoding, res.Body)
if err != nil {
utils.Logger.Errorf("[SCORE] Failed to decompress response body: %s", err.Error())
return nil, 0
}
defer reader.Close()
var response models.TrackerDataJSON
body, err := io.ReadAll(reader)
if err != nil {
utils.Logger.Errorf("[SCORE] Failed to read response body: %s", err.Error())
return nil, 0
}
if err := json.Unmarshal(body, &response); err != nil {
utils.Logger.Errorf("[SCORE] Failed to deserialize tracker API response: %s", err)
return nil, 0
}
return &response, 200
}
func calcPlayerScoreNew(playerData *models.TrackerDataJSON) float32 {
const KdFactor = 0.5
const KpmFactor = 0.3
const ObjectiveFactor = 0.15
const SupportFactor = 0.05
const NormalizeFactor = 1.3
cleanedKills := playerData.DivKills.ADS + playerData.DivKills.Hip - playerData.DivKills.AI
kd := float64(cleanedKills) / float64(playerData.Deaths)
kpm := float64(cleanedKills) / float64(playerData.SecondsPlayed/60.0)
objective := float64(playerData.XP[0].Ribbons.Objective) / float64(playerData.XP[0].Ribbons.Total)
support := float64(playerData.XP[0].Ribbons.Support+playerData.XP[0].Ribbons.Squad) / float64(playerData.XP[0].Ribbons.Total)
score := (kd * KdFactor) + (kpm * KpmFactor) + (objective * ObjectiveFactor) + (support * SupportFactor)
score *= NormalizeFactor
return float32(score)
}
func calcPlayerScore(playerData *models.TrackerDataJSON) float32 {
gameMetrics := GetGameMetric("BF2042")
if gameMetrics == nil {
utils.Logger.Errorf("[SCORE] No game metrics specified for '%s'", "BF2042")
return -1
}
normalizeFactor := gameMetrics.NormalizeFactor
topWeaponCount := gameMetrics.TopWeaponCount
sort.SliceStable(playerData.Weapons, func(i, j int) bool { return playerData.Weapons[i].Kills > playerData.Weapons[j].Kills })
var top []models.Weapon
for _, weapon := range playerData.Weapons {
if len(top) >= topWeaponCount || weapon.Kills < 100 {
break
}
acc := (float64(weapon.ShotsHit) / float64(weapon.ShotsFired)) * 100
kpm := weapon.KPM
weaponMetrics := FindWeaponMetric(gameMetrics.WeaponMetrics, weapon.Type)
if weaponMetrics == nil {
utils.Logger.Errorf("[SCORE] No weapon metrics specified for '%s'", weapon.Type)
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)
}

View File

@@ -2,44 +2,38 @@ package controllers
import ( import (
"InfantrySkillCalculator/models" "InfantrySkillCalculator/models"
"log" "InfantrySkillCalculator/utils"
"github.com/gin-gonic/gin"
"session"
) )
func CreateUser(username string, hashedPassword string, enabled bool, usedCode string) { func CreateUser(username string, hashedPassword string, enabled bool, usedCode string) {
user := models.User{Username: username, Password: hashedPassword, Enabled: enabled} user := models.User{Username: username, Password: hashedPassword, Enabled: enabled}
err := models.DB.Create(&user).Error var code models.ActivationCode
if err != nil { var bf2042 models.Game
log.Fatalf("Error while creating user: %v", err)
if err := models.DB.Create(&user).Error; err != nil {
utils.Logger.Fatalf("[USER] Error while creating user: %s", err.Error())
} }
var code models.ActivationCode if err := models.DB.Where("code = ?", usedCode).First(&code).Error; err != nil {
err = models.DB. utils.Logger.Fatalf("[USER] Error while getting activation code: %s", err.Error())
Model(&models.ActivationCode{}).
Where("code = ?", usedCode).
First(&code).Error
if err != nil {
log.Fatalf("Error while getting activation code: %v", err)
} }
code.UsedForUsername = username code.UsedForUsername = username
err = models.DB.Save(&code).Error if err := models.DB.Save(&code).Error; err != nil {
if err != nil { utils.Logger.Fatalf("[USER] Error while updating activation code: %s", err.Error())
log.Fatalf("Error while updating activation code: %v", err)
} }
user.UserRole = code.UserRole user.UserRole = code.UserRole
err = models.DB.Save(&user).Error if err := models.DB.Save(&user).Error; err != nil {
if err != nil { utils.Logger.Fatalf("[USER] Error while updating user role: %s", err.Error())
log.Fatalf("Error while updating user role: %v", err)
} }
var bf2042 models.Game if err := models.DB.Where("tag = ?", "BF2042").First(&bf2042).Error; err != nil {
err = models.DB. utils.Logger.Fatalf("[USER] Error while getting game: %v", err)
Where("tag = ?", "BF2042").
First(&bf2042).Error
if err != nil {
log.Fatalf("Error while getting game: %v", err)
} }
userSettings := models.UserSettings{ userSettings := models.UserSettings{
Username: username, Username: username,
ActiveGameID: bf2042.ID, ActiveGameID: bf2042.ID,
@@ -47,21 +41,51 @@ func CreateUser(username string, hashedPassword string, enabled bool, usedCode s
CalcMedian: false, CalcMedian: false,
UseCache: true, UseCache: true,
} }
models.DB.Create(&userSettings)
if err := models.DB.Create(&userSettings).Error; err != nil {
utils.Logger.Fatalf("[USER] Error while creating user settings: %s", err.Error())
}
utils.Logger.Infof("[USER] Created user: %s", username)
} }
func IsUserEnabled(username string) bool { func IsUserEnabled(username string) bool {
var user models.User var user models.User
models.DB.Where("username = ?", username).First(&user)
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
utils.Logger.Warnf("[USER] IsUserEnabled: Error while getting user: %s", err.Error())
return false
}
return user.Enabled return user.Enabled
} }
func GetUserRole(username string) models.Role {
var user models.User
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
utils.Logger.Fatalf("[USER] GetUserRole: Error while getting user: %s", err.Error())
}
return user.UserRole
}
func GetUserRoleByCtx(c *gin.Context) models.Role {
username, ok := session.GetUsername(c)
if !ok {
return models.ReaderRole
}
return GetUserRole(username)
}
func IsUserAdmin(username string) bool { func IsUserAdmin(username string) bool {
var user models.User var user models.User
err := models.DB.Where("username = ?", username).First(&user).Error
if err != nil { if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
log.Fatal(err) utils.Logger.Fatalf("[USER] IsUserAdmin: Error while getting user: %s", err.Error())
return false
} }
return user.UserRole == models.AdminRole return user.UserRole == models.AdminRole
} }

View File

@@ -2,7 +2,7 @@ package controllers
import ( import (
"InfantrySkillCalculator/models" "InfantrySkillCalculator/models"
"errors" "InfantrySkillCalculator/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"session" "session"
@@ -22,11 +22,13 @@ func GetSettings(c *gin.Context) {
username, ok := session.GetUsername(c) username, ok := session.GetUsername(c)
if !ok { if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"})
utils.Logger.Errorf("[SETTINGS] User not logged in: %s", username)
return return
} }
if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil { if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"})
utils.Logger.Errorf("[SETTINGS] No settings available for user %s", username)
return return
} }
@@ -40,30 +42,6 @@ func GetSettings(c *gin.Context) {
c.JSON(http.StatusOK, sanitizedSettings) 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 // UpdateSettings PATCH /settings
func UpdateSettings(c *gin.Context) { func UpdateSettings(c *gin.Context) {
var settings models.UserSettings var settings models.UserSettings
@@ -71,21 +49,28 @@ func UpdateSettings(c *gin.Context) {
username, ok := session.GetUsername(c) username, ok := session.GetUsername(c)
if !ok { if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"})
utils.Logger.Errorf("[SETTINGS] User not logged in: %s", username)
return return
} }
if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil { if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"}) c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"})
utils.Logger.Errorf("[SETTINGS] No settings available for user %s", username)
return return
} }
var input UpdateSettingsInput var input UpdateSettingsInput
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[SETTINGS] Failed to bind JSON: %s", err.Error())
return return
} }
models.DB.Model(&settings).Updates(input) if err := models.DB.Model(&settings).Updates(input).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Logger.Errorf("[SETTINGS] Failed to update settings: %s", err.Error())
return
}
c.JSON(http.StatusOK, nil) c.JSON(http.StatusOK, nil)
} }

View File

@@ -7,7 +7,7 @@ services:
- "127.0.0.1:8000:8000" - "127.0.0.1:8000:8000"
environment: environment:
- GO_ENV=production - GO_ENV=production
- REDIS_ADDRESS=redis - REDIS_ADDRESS=redis:6379
depends_on: depends_on:
- redis - redis
volumes: volumes:
@@ -15,4 +15,7 @@ services:
redis: redis:
image: redis:alpine image: redis:alpine
ports: ports:
- "127.0.0.1:6379:6379" - "127.0.0.1:6379:6379"
command: redis-server --save 5 1 --loglevel warning
volumes:
- ./redis:/data

58
go.mod
View File

@@ -1,58 +1,66 @@
module InfantrySkillCalculator module InfantrySkillCalculator
go 1.21 go 1.23
toolchain go1.23.6
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.10.0
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.4.0
github.com/redis/go-redis/v9 v9.4.0 golang.org/x/crypto v0.33.0
golang.org/x/crypto v0.9.0 gorm.io/gorm v1.25.12
gorm.io/gorm v1.25.5
) )
require internal/cache v1.0.0
replace internal/cache => ./internal/cache replace internal/cache => ./internal/cache
require ( require (
github.com/glebarez/sqlite v1.10.0 github.com/glebarez/sqlite v1.11.0
github.com/sirupsen/logrus v1.9.3
gopkg.in/natefinch/lumberjack.v2 v2.2.1
internal/session v1.0.0 internal/session v1.0.0
) )
require (
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
)
replace internal/session => ./internal/session replace internal/session => ./internal/session
replace internal/burst_queue => ./internal/burst_queue
require ( require (
github.com/bytedance/sonic v1.9.1 // indirect github.com/andybalholm/brotli v1.1.1
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/bytedance/sonic v1.11.6 // 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/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
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/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/compress v1.17.11
github.com/leodido/go-urn v1.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.10.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.8.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect

117
go.sum
View File

@@ -1,43 +1,38 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 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-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.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= 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/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.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 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/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -49,76 +44,83 @@ 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/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 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
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=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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-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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= 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 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
@@ -127,4 +129,5 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 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 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -1,7 +1,10 @@
go 1.21 go 1.23
toolchain go1.23.6
use ( use (
. .
internal/burst_queue
internal/cache internal/cache
internal/session internal/session
) )

View File

@@ -1,15 +1,56 @@
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 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/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= 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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= 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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 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 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
@@ -28,4 +69,5 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 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 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=

View File

@@ -0,0 +1,88 @@
package burst_queue
import (
"log"
"sync"
"time"
)
// BurstQueue is a queue that will flush its entries when it reaches a certain size or after a certain timeout.
type BurstQueue struct {
maxQueueSize uint
burstTimeout time.Duration
burstCallback func(entries []interface{}, elapsed time.Duration)
entries []interface{}
startTime time.Time
flushMu sync.Mutex
timerMu sync.Mutex
}
// NewBurstQueue creates a new BurstQueue.
// maxQueueSize is the maximum number of entries to queue before flushing.
// burstTimeout is the maximum time to wait before flushing.
// burstCallback is the function to call when flushing.
func NewBurstQueue(maxQueueSize uint, burstTimeout time.Duration, burstCallback func(entries []interface{}, elapsed time.Duration)) *BurstQueue {
return &BurstQueue{
maxQueueSize: maxQueueSize,
burstTimeout: burstTimeout,
burstCallback: burstCallback,
}
}
// startTimeout starts the timeout.
func (bq *BurstQueue) startTimeout() {
newStart := time.Now()
bq.timerMu.Lock()
bq.startTime = newStart
bq.timerMu.Unlock()
time.Sleep(bq.burstTimeout)
bq.timerMu.Lock()
shouldFlush := bq.startTime.Equal(newStart) && !bq.startTime.IsZero()
bq.timerMu.Unlock()
if shouldFlush {
bq.FlushEntries()
}
}
// Add adds an entry to the queue and restarts the timeout.
func (bq *BurstQueue) Add(entry interface{}) {
go bq.startTimeout()
bq.entries = append(bq.entries, entry)
if len(bq.entries) >= int(bq.maxQueueSize) {
bq.FlushEntries()
}
}
// FlushEntries flushes the queue.
func (bq *BurstQueue) FlushEntries() {
bq.timerMu.Lock()
if bq.startTime.IsZero() || bq.entries == nil {
bq.timerMu.Unlock()
return // not started yet or already flushed
}
actualTimeout := time.Since(bq.startTime)
bq.startTime = time.Time{}
bq.timerMu.Unlock()
bq.flushMu.Lock()
bq.burstCallback(bq.entries, actualTimeout)
bq.entries = nil
bq.flushMu.Unlock()
}
func ConvertSlice[T any](src []interface{}) []T {
dst := make([]T, len(src))
for i, v := range src {
var ok bool
if dst[i], ok = v.(T); !ok {
log.Fatalf("item at index %d is not of type %T", i, dst[i])
}
}
return dst
}

View File

@@ -0,0 +1,3 @@
module burst_queue
go 1.23

View File

@@ -1,18 +1,59 @@
package cache package cache
import ( import (
"InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"log"
"strconv"
"time"
) )
var ctx = context.Background() var ctx = context.Background()
func GetValue(key string) (string, error) { type PlayerCache struct {
val, err := models.Cache.Get(ctx, key).Result() cache *redis.Client
lifetime time.Duration
expireSub *redis.PubSub
expireCallback func(key string)
}
func NewPlayerCache(address string, lifetime time.Duration, expireCallback func(key string)) *PlayerCache {
pc := &PlayerCache{
redis.NewClient(
&redis.Options{
Addr: address,
Password: "",
DB: 0,
}),
lifetime,
nil,
expireCallback,
}
pc.expireSub = pc.cache.PSubscribe(ctx, "__keyevent@0__:expired")
go func() {
for {
msg, err := pc.expireSub.ReceiveMessage(ctx)
if err != nil {
log.Fatal(err)
} else {
pc.expireCallback(msg.Payload)
}
}
}()
return pc
}
func (pc *PlayerCache) Connect() error {
_, err := pc.cache.Ping(ctx).Result()
return err
}
func (pc *PlayerCache) GetValue(key string) (string, error) {
val, err := pc.cache.Get(ctx, key).Result()
if err != nil { if err != nil {
return "", err // cache miss or error return "", err // cache miss or error
} else { } else {
@@ -20,36 +61,71 @@ func GetValue(key string) (string, error) {
} }
} }
func SetValue(key string, value interface{}) error { func (pc *PlayerCache) SetValue(key string, value interface{}, lifetime time.Duration) error {
return models.Cache.Set(ctx, key, value, utils.PlayerCacheLifetime).Err() return pc.cache.Set(ctx, key, value, lifetime).Err()
} }
func SetScore(playerId uint, gameTag string, score float32) error { func (pc *PlayerCache) SetScore(playerId uint, score float32) error {
key := GetPlayerCacheKey(playerId, gameTag) key := getPlayerCacheKey(playerId)
return SetValue(key, score) return pc.SetValue(key, score, pc.lifetime)
} }
func GetScore(playerId uint, gameTag string) (float32, error) { func (pc *PlayerCache) GetScore(playerId uint) (float32, error) {
key := GetPlayerCacheKey(playerId, gameTag) key := getPlayerCacheKey(playerId)
val, err := GetValue(key) val, err := pc.GetValue(key)
if errors.Is(err, redis.Nil) { if errors.Is(err, redis.Nil) {
return -1.0, nil // cache miss return -1.0, nil // cache miss
} else if err != nil { } else if err != nil {
return -1.0, err // cache error return -1.0, err // cache error
} else { } else {
return utils.StringToFloat(val), nil // cache hit valFloat, _ := strconv.ParseFloat(val, 32)
return float32(valFloat), nil // cache hit
} }
} }
func DeleteScore(playerId uint, gameTag string) error { func (pc *PlayerCache) GetScores(playerIds []uint) ([]float32, error) {
key := GetPlayerCacheKey(playerId, gameTag) vals, err := pc.cache.Pipelined(ctx, func(pipe redis.Pipeliner) error {
return models.Cache.Del(ctx, key).Err() for _, id := range playerIds {
key := getPlayerCacheKey(id)
pipe.Get(ctx, key)
}
return nil
})
if err != nil && !errors.Is(err, redis.Nil) {
return nil, err
}
var scores []float32
for _, val := range vals {
score, err := val.(*redis.StringCmd).Float32()
if errors.Is(err, redis.Nil) { // cache miss
score = -1
err = nil
} else if err != nil { // cache error
score = -1
}
scores = append(scores, score)
}
return scores, nil
} }
func PurgeCache() error { func (pc *PlayerCache) DeleteScore(playerId uint) error {
return models.Cache.FlushAll(ctx).Err() key := getPlayerCacheKey(playerId)
return pc.cache.Del(ctx, key).Err()
} }
func GetPlayerCacheKey(playerId uint, gameTag string) string { func (pc *PlayerCache) PurgeCache() error {
return fmt.Sprintf("player:%d:game:%s", playerId, gameTag) return pc.cache.FlushAll(ctx).Err()
}
func getPlayerCacheKey(playerId uint) string {
return fmt.Sprintf("player:%d", playerId)
}
func (pc *PlayerCache) GetPlayerIdFromCacheKey(key string) (uint, error) {
var playerId uint
_, err := fmt.Sscanf(key, "player:%d", &playerId)
return playerId, err
} }

View File

@@ -1,3 +1,10 @@
module cache module cache
go 1.21 go 1.23
require github.com/redis/go-redis/v9 v9.4.0
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

10
internal/cache/go.sum vendored Normal file
View File

@@ -0,0 +1,10 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/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=

View File

@@ -1,3 +1,3 @@
module session module session
go 1.21 go 1.23

160
main.go
View File

@@ -6,19 +6,35 @@ import (
"InfantrySkillCalculator/utils" "InfantrySkillCalculator/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
_ "github.com/gorilla/sessions" _ "github.com/gorilla/sessions"
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
"html/template" "html/template"
"io" "io"
"log"
"os" "os"
"session"
"strings"
"time"
) )
var mainPageTemplates *template.Template
var loginPageTemplates *template.Template
var registerPageTemplates *template.Template
func init() { func init() {
fileLogger := &lumberjack.Logger{
Filename: "isc_rest.log",
MaxSize: 50, // megabytes
MaxBackups: 3,
MaxAge: 28, //days
Compress: true,
}
ginWriter := io.MultiWriter(fileLogger, os.Stdout)
logger := logrus.New()
logger.SetOutput(ginWriter)
logger.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
FullTimestamp: true,
})
utils.Logger = logger
var err error var err error
mainPageTemplates, err = template.ParseFiles( utils.MainPageTemplates, err = template.ParseFiles(
"./templates/index.html", "./templates/index.html",
"./templates/components/home_clan_bar.html", "./templates/components/home_clan_bar.html",
"./templates/components/opp_clan_bar.html", "./templates/components/opp_clan_bar.html",
@@ -31,32 +47,71 @@ func init() {
"./templates/modals/add_player.html", "./templates/modals/add_player.html",
"./templates/modals/delete_player.html", "./templates/modals/delete_player.html",
"./templates/modals/edit_player.html", "./templates/modals/edit_player.html",
"./templates/modals/settings.html", "./templates/modals/single_calc.html",
//"./templates/modals/settings.html",
"./templates/modals/full_calc.html",
"./templates/components/header.html", "./templates/components/header.html",
) )
if err != nil { if err != nil {
log.Fatal(err) logger.Fatalf("[MAIN] Error parsing main page templates: %v", err)
} }
loginPageTemplates, err = template.ParseFiles( utils.LoginPageTemplates, err = template.ParseFiles(
"./templates/login.html", "./templates/login.html",
"./templates/components/header.html", "./templates/components/header.html",
) )
if err != nil { if err != nil {
log.Fatal(err) logger.Fatalf("[MAIN] Error parsing login page templates: %v", err)
} }
registerPageTemplates, err = template.ParseFiles( utils.RegisterPageTemplates, err = template.ParseFiles(
"./templates/register.html", "./templates/register.html",
"./templates/components/header.html", "./templates/components/header.html",
) )
if err != nil { if err != nil {
log.Fatal(err) logger.Fatalf("[MAIN] Error parsing register page templates: %v", err)
}
utils.PlayerItemTemplate, err = template.ParseFiles(
"./templates/shards/player_list_item.html",
)
if err != nil {
logger.Fatalf("[MAIN] Error parsing player list item template: %v", err)
} }
controllers.LoadMetrics() controllers.LoadMetrics()
} }
func customLogrusLogger(logger *logrus.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
elapsed := time.Since(start)
if c.Request.URL.Path == "/static" || c.Request.URL.Path == "/favicon.ico" ||
strings.Contains(c.Request.URL.Path, "_html") {
return
}
username, exists := session.GetUsername(c)
if !exists {
username = "Anonymous"
}
logger.Infof("[GIN] %3d | %10.3fms | %15s | %15s | %15s | %-7s | %s",
c.Writer.Status(),
float64(elapsed.Microseconds())/1000.0,
c.ClientIP(),
c.Request.Header.Get("X-Forwarded-For"),
username,
c.Request.Method,
c.Request.RequestURI,
)
}
}
func main() { func main() {
if os.Getenv("GO_ENV") == "production" { if os.Getenv("GO_ENV") == "production" {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
@@ -65,79 +120,76 @@ func main() {
router := gin.New() router := gin.New()
err := router.SetTrustedProxies([]string{"127.0.0.1"}) err := router.SetTrustedProxies([]string{"127.0.0.1"})
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatalf("[MAIN] Error setting trusted proxies: %v", err)
} }
router.LoadHTMLGlob("templates/**/*") router.LoadHTMLGlob("templates/**/*")
protected := router.Group("/") reader := router.Group("/")
protected.Use(AuthRequired()) reader.Use(ReaderAuthRequired())
author := router.Group("/")
author.Use(AuthorAuthRequired())
admin := router.Group("/admin") admin := router.Group("/admin")
admin.Use(AdminAuthRequired()) admin.Use(AdminAuthRequired())
models.ConnectDatabase() models.ConnectDatabase()
models.ConnectCache() models.ConnectCache(utils.PlayerCacheLifetime, controllers.UpdateCacheAfterExpiry)
var code models.ActivationCode var code models.ActivationCode
if err := models.DB.First(&code).Error; err != nil { if err := models.DB.First(&code).Error; err != nil {
firstCode := utils.GenerateActivationCode() firstCode := utils.GenerateActivationCode()
models.DB.Create(&models.ActivationCode{Code: firstCode, UserRole: models.AdminRole}) models.DB.Create(&models.ActivationCode{Code: firstCode, UserRole: models.AdminRole})
log.Println("Created first activation code with ADMIN role:\n" + firstCode) utils.Logger.Println("[MAIN] 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(
gin.LoggerWithWriter(utils.GinWriter, "/static"),
gin.Recovery(),
)
protected.Use(
gin.LoggerWithWriter(utils.GinWriter),
gin.Recovery(),
)
admin.Use(
gin.LoggerWithWriter(utils.GinWriter),
gin.Recovery(),
)
router.Static("/static", "./static") router.Static("/static", "./static")
middlewares := []gin.HandlerFunc{
customLogrusLogger(utils.Logger),
gin.Recovery(),
}
router.Use(middlewares...)
reader.Use(middlewares...)
author.Use(middlewares...)
admin.Use(middlewares...)
router.GET("/login", loginPage) router.GET("/login", loginPage)
router.POST("/login", loginPost) router.POST("/login", loginPost)
router.GET("/logout", logout) router.GET("/logout", logout)
router.GET("/register", registerPage) router.GET("/register", registerPage)
router.POST("/register", registerPost) router.POST("/register", registerPost)
protected.GET("/", mainPage) reader.GET("/", mainPage)
protected.GET("/clans", controllers.GetAllClans) reader.GET("/clans", controllers.GetAllClans)
protected.GET("/clans_html", controllers.GetAllClansHTML) reader.GET("/clans_html", controllers.GetAllClansHTML)
protected.GET("/clan/:id", controllers.GetClanByID) reader.GET("/clan/:id", controllers.GetClanByID)
protected.POST("/clan", controllers.AddClan) author.POST("/clan", controllers.AddClan)
protected.PATCH("/clan/:id", controllers.UpdateClanByID) author.PATCH("/clan/:id", controllers.UpdateClanByID)
protected.DELETE("/clan/:id", controllers.DeleteClanByID) author.DELETE("/clan/:id", controllers.DeleteClanByID)
protected.GET("/players", controllers.GetAllPlayers) reader.GET("/players", controllers.GetAllPlayers)
protected.GET("/players_html", controllers.GetPlayersByClanHTML) reader.GET("/players_html", controllers.GetPlayersByClanHTML)
protected.GET("/player/:id", controllers.GetPlayerByID) reader.GET("/player/:id", controllers.GetPlayerByID)
protected.GET("/playerid/:name", controllers.GetPlayerIDByName) author.POST("/player", controllers.AddPlayer)
protected.POST("/player", controllers.AddPlayer) author.PATCH("/player/:id", controllers.UpdatePlayerByID)
protected.PATCH("/player/:id", controllers.UpdatePlayerByID) author.DELETE("/player/:id", controllers.DeletePlayerByID)
protected.DELETE("/player/:id", controllers.DeletePlayerByID)
protected.GET("/cache/:player_id", controllers.GetCacheByPlayerID) reader.GET("/cache/:player_id", controllers.GetCacheByPlayerID)
protected.GET("/score/:player_id", controllers.GetScoreByPlayerID) reader.GET("/score/:player_id", controllers.GetScoreByPlayerID)
reader.POST("/score/:player_name", controllers.GetScoreByPlayerName)
protected.GET("/game", controllers.GetGames) reader.GET("/game", controllers.GetGames)
protected.GET("/game_html", controllers.GetGamesHTML) reader.GET("/game_html", controllers.GetGamesHTML)
protected.GET("/settings", controllers.GetSettings) reader.GET("/settings", controllers.GetSettings)
protected.PATCH("/settings", controllers.UpdateSettings) reader.PATCH("/settings", controllers.UpdateSettings)
admin.DELETE("/clear_cache", controllers.DeleteAllCaches) admin.DELETE("/clear_cache", controllers.DeleteAllCaches)
admin.DELETE("/purge_players", controllers.DeleteAllPlayers) admin.DELETE("/purge_players", controllers.DeleteAllPlayers)
admin.DELETE("/purge_clans", controllers.DeleteAllClans) admin.DELETE("/purge_clans", controllers.DeleteAllClans)
admin.POST("/create_code", controllers.CreateCode) admin.POST("/create_code", controllers.CreateCode)
log.Println("Running on 8000...") utils.Logger.Println("[MAIN] Running on 8000...")
log.Fatal(router.Run(":8000")) utils.Logger.Fatalf("[MAIN] %v", router.Run(":8000"))
} }

View File

@@ -2,7 +2,7 @@ package models
type Clan struct { type Clan struct {
ID uint `json:"id" gorm:"primary_key"` ID uint `json:"id" gorm:"primary_key"`
Name string `json:"name"` Name string `json:"name" gorm:"unique"`
Tag string `json:"tag"` Tag string `json:"tag" gorm:"unique"`
KeepUpdated bool `json:"keep_updated"` KeepUpdated bool `json:"keep_updated"`
} }

View File

@@ -2,7 +2,7 @@ package models
type Player struct { type Player struct {
ID uint `json:"id" gorm:"primary_key"` ID uint `json:"id" gorm:"primary_key"`
Name string `json:"name"` Name string `json:"name" gorm:"not null"`
ClanID uint `json:"clan_id"` ClanID uint `json:"clan_id" gorm:"not null"`
Clan Clan `gorm:"references:ID"` Clan Clan `gorm:"references:ID"`
} }

View File

@@ -1,18 +1,17 @@
package models package models
import ( import (
"context" "InfantrySkillCalculator/utils"
"cache"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/redis/go-redis/v9"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"log"
"os" "os"
"time"
) )
var DB *gorm.DB var DB *gorm.DB
var ctx = context.Background() var PlayerCache *cache.PlayerCache
var Cache *redis.Client
func ConnectDatabase() { func ConnectDatabase() {
database, err := gorm.Open(sqlite.Open("isc_data.db"), &gorm.Config{ database, err := gorm.Open(sqlite.Open("isc_data.db"), &gorm.Config{
@@ -22,64 +21,54 @@ func ConnectDatabase() {
}) })
if err != nil { if err != nil {
panic("Failed to connect to database! " + err.Error()) utils.Logger.Fatal("[SETUP] Failed to connect to database! " + err.Error())
} }
err = database.AutoMigrate(&Clan{}) err = database.AutoMigrate(&Clan{})
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
} }
err = database.AutoMigrate(&Player{}) err = database.AutoMigrate(&Player{})
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
} }
err = database.AutoMigrate(&User{}) err = database.AutoMigrate(&User{})
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
} }
err = database.AutoMigrate(&ActivationCode{}) err = database.AutoMigrate(&ActivationCode{})
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
} }
err = database.AutoMigrate(&Game{}) err = database.AutoMigrate(&Game{})
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
} else { } else {
var game Game var game Game
if err := database.First(&game).Error; err != nil { if err := database.First(&game).Error; err != nil {
database.Create(&Game{Name: "Battlefield V", Tag: "BFV"}) database.Create(&Game{Name: "Battlefield V", Tag: "BFV"})
database.Create(&Game{Name: "Battlefield 2042", Tag: "BF2042"}) database.Create(&Game{Name: "Battlefield 2042", Tag: "BF2042"})
log.Println("Created first games") utils.Logger.Println("[SETUP] Created first games")
} }
} }
err = database.AutoMigrate(&UserSettings{}) err = database.AutoMigrate(&UserSettings{})
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
} }
DB = database DB = database
} }
func ConnectCache() { func ConnectCache(playerCacheLifetime time.Duration, expireCallback func(key string)) {
address := os.Getenv("REDIS_ADDRESS") address := os.Getenv("REDIS_ADDRESS")
if address == "" { if address == "" {
address = "127.0.0.1" address = "127.0.0.1:6379"
}
port := os.Getenv("REDIS_PORT")
if port == "" {
port = "6379"
} }
Cache = redis.NewClient(&redis.Options{ PlayerCache = cache.NewPlayerCache(address, playerCacheLifetime, expireCallback)
Addr: address + ":" + port, if err := PlayerCache.Connect(); err != nil {
Password: "", PlayerCache = nil
DB: 0, utils.Logger.Fatal("[SETUP] Failed to connect to Redis! " + err.Error())
})
_, err := Cache.Ping(ctx).Result()
if err != nil {
Cache = nil
log.Fatal("Failed to connect to Redis! " + err.Error())
} }
} }

View File

@@ -1,7 +1,28 @@
package models package models
type TrackerWeaponJSON struct { type TrackerDataJSON struct {
Weapons []Weapon `json:"weapons"` Weapons []Weapon `json:"weapons"`
DivKills DividedKills `json:"dividedKills"`
Deaths int `json:"deaths"`
SecondsPlayed int `json:"secondsPlayed"`
XP []XP `json:"XP"`
}
type DividedKills struct {
ADS int `json:"ads"`
Hip int `json:"hip"`
AI int `json:"ai"`
}
type XP struct {
Ribbons Ribbons `json:"ribbons"`
}
type Ribbons struct {
Objective int `json:"objective"`
Squad int `json:"squad"`
Support int `json:"support"`
Total int `json:"total"`
} }
type Weapon struct { type Weapon struct {

View File

@@ -2,20 +2,26 @@ package main
import ( import (
"InfantrySkillCalculator/controllers" "InfantrySkillCalculator/controllers"
"InfantrySkillCalculator/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"log"
"net/http" "net/http"
"session" "session"
) )
func mainPage(c *gin.Context) { func mainPage(c *gin.Context) {
data := map[string]interface{}{ username, ok := session.GetUsername(c)
"isAdmin": isUserAdmin(c), if !ok {
username = ""
} }
err := mainPageTemplates.Execute(c.Writer, data) data := map[string]interface{}{
"UserRole": controllers.GetUserRoleByCtx(c),
"Username": username,
}
err := utils.MainPageTemplates.Execute(c.Writer, data)
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatalf("[MAIN] Error while executing template: %s", err.Error())
} }
} }
@@ -25,9 +31,9 @@ func loginPage(c *gin.Context) {
return return
} }
err := loginPageTemplates.Execute(c.Writer, nil) err := utils.LoginPageTemplates.Execute(c.Writer, nil)
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatalf("[LOGIN] Error while executing template: %s", err.Error())
} }
} }
@@ -37,11 +43,13 @@ func loginPost(c *gin.Context) {
if !checkUserCredentials(username, password) { if !checkUserCredentials(username, password) {
c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Ungültige Logindaten!"}) c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Ungültige Logindaten!"})
utils.Logger.Warnf("[LOGIN] User %s tried to login with invalid credentials", username)
return return
} }
if err := session.SetLoginSession(username, c); err != nil { if err := session.SetLoginSession(username, c); err != nil {
c.JSON(http.StatusInternalServerError, nil) c.JSON(http.StatusInternalServerError, nil)
utils.Logger.Errorf("[LOGIN] Error while setting login session: %s", err.Error())
return return
} }
@@ -52,6 +60,7 @@ func loginPost(c *gin.Context) {
func logout(c *gin.Context) { func logout(c *gin.Context) {
if err := session.InvalidateSession(c); err != nil { if err := session.InvalidateSession(c); err != nil {
c.JSON(http.StatusInternalServerError, nil) c.JSON(http.StatusInternalServerError, nil)
utils.Logger.Errorf("[LOGOUT] Error while invalidating session: %s", err.Error())
return return
} }
@@ -64,9 +73,9 @@ func registerPage(c *gin.Context) {
return return
} }
err := registerPageTemplates.Execute(c.Writer, nil) err := utils.RegisterPageTemplates.Execute(c.Writer, nil)
if err != nil { if err != nil {
log.Fatal(err) utils.Logger.Fatalf("[REGISTER] Error while executing template: %s", err.Error())
} }
} }
@@ -77,12 +86,14 @@ func registerPost(c *gin.Context) {
if !isValidCode(code) { if !isValidCode(code) {
c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Ungültiger Aktivierungscode!"}) c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Ungültiger Aktivierungscode!"})
utils.Logger.Warnf("[REGISTER] User %s tried to register with invalid code %s", username, code)
return return
} }
hashedPassword, err := hashPassword(password) hashedPassword, err := hashPassword(password)
if err != nil { if err != nil {
c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Fehler beim Registrieren!"}) c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Fehler beim Registrieren!"})
utils.Logger.Errorf("[REGISTER] Error while hashing password: %s", err.Error())
return return
} }
@@ -90,6 +101,7 @@ func registerPost(c *gin.Context) {
if err := session.SetLoginSession(username, c); err != nil { if err := session.SetLoginSession(username, c); err != nil {
c.JSON(http.StatusInternalServerError, nil) c.JSON(http.StatusInternalServerError, nil)
utils.Logger.Errorf("[REGISTER] Error while setting login session: %s", err.Error())
return return
} }

62
static/dialogs.js Normal file
View File

@@ -0,0 +1,62 @@
const swalClasses = {
container: 'text-center',
confirmButton: 'btn btn-lg btn-primary',
cancelButton: 'btn btn-lg btn-secondary ms-3',
denyButton: 'btn btn-lg btn-secondary ms-3',
popup: 'border p-5',
title: 'fs-2',
inputLabel: 'fs-5 text-secondary-emphasis',
htmlContainer: 'fs-5 text-center text-secondary-emphasis',
};
function showAdminActionExecutedDialog(xhr, method) {
Swal.fire({
title: 'Action executed',
text: xhr.response,
icon: xhr.status === 200 ? 'success' : 'error'
}).then(() => {
if (method === "delete") {
location.reload();
}
});
}
function confirmAndTrigger(btn) {
Swal.fire({
title: btn.innerText,
text: 'Do you want to continue?',
confirmButtonText: 'Yes',
confirmButtonColor: '#dd6b55',
denyButtonColor: '#3085d6',
icon: 'warning',
showDenyButton: true,
customClass: swalClasses,
buttonsStyling: false
}).then((result) => {
if (result.isConfirmed) {
htmx.trigger(btn, 'confirmed', null);
}
});
}
function createCodeDialog(btn) {
Swal.fire({
title: btn.innerText,
input: 'select',
inputOptions: {
'READER': 'Reader',
'AUTHOR': 'Author',
'ADMIN': 'Admin'
},
inputPlaceholder: 'Select a role',
confirmButtonText: 'Generate',
showCancelButton: true,
customClass: swalClasses,
buttonsStyling: false
}).then((result) => {
if (result.isConfirmed) {
btn.setAttribute('hx-vals', '{"user_role": "' + result.value + '"}');
htmx.trigger(btn, 'confirmed', null);
}
});
}

12
static/icons/average.svg Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>average</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="drop" fill="#000000" transform="translate(60.496777, 60.491376)">
<path d="M362.038672,4.40536496e-13 L392.208561,30.1698893 L330.849151,91.530257 C352.999344,120.319635 366.169889,156.376056 366.169889,195.508624 C366.169889,289.765221 289.75982,366.17529 195.503223,366.17529 C156.370421,366.17529 120.313802,353.004587 91.5243389,330.854154 L30.1698893,392.208561 L-1.42108547e-14,362.038672 L61.2047929,300.834525 C38.4229489,271.826458 24.836556,235.254177 24.836556,195.508624 C24.836556,101.252026 101.246625,24.841957 195.503223,24.841957 C235.248317,24.841957 271.820213,38.4280364 300.82812,61.2094052 L362.038672,4.40536496e-13 Z M300.334816,122.043508 L122.038107,300.340217 C142.831803,314.939228 168.166768,323.508624 195.503223,323.508624 C266.195671,323.508624 323.503223,266.201072 323.503223,195.508624 C323.503223,168.172169 314.933827,142.837204 300.334816,122.043508 Z M195.503223,67.5086237 C124.810775,67.5086237 67.5032227,124.816176 67.5032227,195.508624 C67.5032227,223.460895 76.4630581,249.320485 91.66741,270.372075 L270.365674,91.6720892 C249.314266,76.4681756 223.455052,67.5086237 195.503223,67.5086237 Z" id="Combined-Shape">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,10 +1,18 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const tooltipTriggerList = document.querySelectorAll('[config-bs-toggle="tooltip"]'); initTooltips(document);
tooltipTriggerList.forEach((elem) => {
new bootstrap.Tooltip(elem);
});
}); });
function initTooltips(elementRoot) {
const tooltipTriggerList = elementRoot.querySelectorAll('[data-bs-action="tooltip"]');
tooltipTriggerList.forEach((elem) => {
new bootstrap.Tooltip(elem, {
trigger: 'hover',
delay: {show: 500, hide: 0},
container: 'body'
});
});
}
function setupClanButtons(dropdownId, delBtnId, editBtnId) { function setupClanButtons(dropdownId, delBtnId, editBtnId) {
const dropdown = document.getElementById(dropdownId); const dropdown = document.getElementById(dropdownId);
const deleteButton = document.getElementById(delBtnId); const deleteButton = document.getElementById(delBtnId);
@@ -111,7 +119,7 @@ function loadClans() {
} }
function updateSelectedPlayers(sender) { function updateSelectedPlayers(sender) {
const playerList = sender.parentElement.parentElement.parentElement; const playerList = sender.parentElement.parentElement.parentElement.parentElement;
const checkCounter = playerList.parentElement.parentElement.querySelector('span.badge'); const checkCounter = playerList.parentElement.parentElement.querySelector('span.badge');
let counter = 0; let counter = 0;
@@ -145,42 +153,6 @@ function deselectAllPlayers(playerListId) {
checkCounter.innerText = 0; checkCounter.innerText = 0;
} }
function confirmAndTrigger(btn) {
Swal.fire({
title: btn.innerText,
text: 'Do you want to continue?',
confirmButtonText: 'Yes',
confirmButtonColor: '#dd6b55',
denyButtonColor: '#3085d6',
icon: 'warning',
showDenyButton: true
}).then((result) => {
if (result.isConfirmed) {
htmx.trigger(btn, 'confirmed');
}
});
}
function createCodeDialog(btn) {
Swal.fire({
title: btn.innerText,
input: 'select',
inputOptions: {
'READER': 'Reader',
'AUTHOR': 'Author',
'ADMIN': 'Admin'
},
inputPlaceholder: 'Select a role',
confirmButtonText: 'Generate',
showCancelButton: true
}).then((result) => {
if (result.isConfirmed) {
btn.setAttribute('hx-vals', '{"user_role": "' + result.value + '"}');
htmx.trigger(btn, 'confirmed');
}
});
}
function singleCalcSpinner(sender) { function singleCalcSpinner(sender) {
const spinner = '<i class="spinner-grow spinner-grow-sm text-info align-baseline me-2" style="margin-left: 0.91rem;" role="status"></i>'; const spinner = '<i class="spinner-grow spinner-grow-sm text-info align-baseline me-2" style="margin-left: 0.91rem;" role="status"></i>';
const score = sender.previousElementSibling.children[1]; const score = sender.previousElementSibling.children[1];
@@ -188,5 +160,13 @@ function singleCalcSpinner(sender) {
sender.disabled = true; sender.disabled = true;
sender.addEventListener('htmx:afterRequest', function () { sender.addEventListener('htmx:afterRequest', function () {
sender.disabled = false; sender.disabled = false;
const homeClan = document.getElementById('home-clan');
const oppClan = document.getElementById('opponent-clan');
if (homeClan.selectedIndex === oppClan.selectedIndex) {
if (sender.parentElement.parentElement.id === 'home-player-list')
oppClan.dispatchEvent(new Event('change'));
else
homeClan.dispatchEvent(new Event('change'));
}
}, {once: true}); }, {once: true});
} }

View File

@@ -1,15 +1,17 @@
{{ define "bottom_controls" }} {{ define "bottom_controls" }}
<div class="row justify-content-between border-top pt-4 position-relative mb-5"> <div class="row justify-content-between border-top border-secondary-subtle pt-4 mx-0 position-relative mb-5">
<div class="col-auto position-absolute start-0"> <div class="col-auto position-absolute start-0">
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<a class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" href="/logout" data-bs-toggle="tooltip" data-bs-title="Abmelden"> <a class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" href="/logout" data-bs-action="tooltip" data-bs-title="Abmelden">
<i class="bi bi-door-closed"></i> <i class="bi bi-door-closed-fill"></i>
</a> </a>
<!--
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" data-bs-toggle="modal" data-bs-target="#settingsModal"> <button class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" data-bs-toggle="modal" data-bs-target="#settingsModal">
<i class="bi bi-gear-fill"></i> <i class="bi bi-gear-fill" data-bs-action="tooltip" data-bs-title="Einstellungen"></i>
</button> </button>
{{ if .isAdmin }} -->
{{ if (eq .UserRole "ADMIN") }}
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis dropdown-toggle" type="button" data-bs-toggle="dropdown"> <button class="btn btn-lg btn-outline-secondary text-secondary-emphasis dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-tools"></i> <i class="bi bi-tools"></i>
@@ -61,14 +63,14 @@
</div> </div>
</div> </div>
<div class="col-auto position-absolute start-50 translate-middle-x"> <div class="col-auto position-absolute start-50 translate-middle-x">
<button class="btn btn-lg btn-outline-primary" type="button" disabled> <button class="btn btn-lg btn-primary" id="fullCalcBtn" data-bs-toggle="modal" data-bs-target="#fullCalcModal" data-bs-action="tooltip" data-bs-title="Ausgewählte Spieler berechnen">
<i class="bi bi-calculator-fill me-2"></i> <i class="bi bi-calculator-fill me-2"></i>
Berechnen Berechnen
</button> </button>
</div> </div>
<div class="col-auto position-absolute end-0"> <div class="col-auto position-absolute end-0">
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis" type="button" disabled> <button class="btn btn-lg btn-outline-secondary text-secondary-emphasis" id="singleCalcBtn" data-bs-toggle="modal" data-bs-target="#singleCalcModal" data-bs-action="tooltip" data-bs-title="Einzelnen Spieler berechnen">
<i class="bi bi-person me-2"></i> <i class="bi bi-person-fill me-2"></i>
Einzel-Abfrage Einzel-Abfrage
</button> </button>
</div> </div>
@@ -76,16 +78,13 @@
<script> <script>
document.body.addEventListener('htmx:afterRequest', function (event) { document.body.addEventListener('htmx:afterRequest', function (event) {
if (event.detail.pathInfo.requestPath.startsWith("/admin/")) { let detail = event.detail;
Swal.fire({ let reqPath = detail.pathInfo.requestPath;
title: 'Action executed', let method = detail.requestConfig.verb;
text: event.detail.xhr.response, let xhr = detail.xhr;
icon: event.detail.xhr.status === 200 ? 'success' : 'error'
}).then(() => { if (reqPath.startsWith("/admin/")) {
if (event.detail.requestConfig.verb === "delete") { showAdminActionExecutedDialog(xhr, method);
location.reload();
}
});
} }
}); });
</script> </script>

View File

@@ -1,16 +1,17 @@
{{ define "header" }} {{ define "header" }}
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Infantry Skill Calculator</title> <title>Infantry Skill Calculator</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="../static/index.js"></script> <script src="../static/index.js"></script>
<!-- Bootstrap 5 CSS --> <script src="../static/dialogs.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-dark@4/dark.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-dark@4/dark.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<link rel="stylesheet" href="../static/index.css"> <link rel="stylesheet" href="../static/index.css">
{{ end }} {{ end }}

View File

@@ -1,7 +1,7 @@
{{ define "home_clan_bar" }} {{ define "home_clan_bar" }}
<div class="row g-2"> <div class="row g-2">
<div class="col-auto"> <div class="col-auto pe-2">
<label for="home-clan" class="col-form-label col-form-label-lg">Clans:</label> <label for="home-clan" class="col-form-label col-form-label-lg">Clans:</label>
</div> </div>
<div class="col"> <div class="col">
@@ -10,15 +10,20 @@
<option disabled selected value>Auswählen...</option> <option disabled selected value>Auswählen...</option>
<!-- Options will be loaded dynamically --> <!-- Options will be loaded dynamically -->
</select> </select>
<button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="home-delete" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#deleteClanModal" disabled> {{ if not (eq .UserRole "READER") }}
<i class="bi bi-trash3" data-bs-toggle="tooltip" data-bs-title="Clan löschen"></i> <button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="home-delete"
</button> data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#deleteClanModal" data-bs-action="tooltip" data-bs-title="Clan löschen" disabled>
<button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#editClanModal" id="home-edit" disabled> <i class="bi bi-trash3-fill"></i>
<i class="bi bi-pencil-fill" data-bs-toggle="tooltip" data-bs-title="Clan bearbeiten"></i> </button>
</button> <button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" id="home-edit"
<button class="btn btn-lg btn-outline-secondary text-success" type="button" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#addClanModal" id="home-add"> data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#editClanModal" data-bs-action="tooltip" data-bs-title="Clan bearbeiten" disabled>
<i class="bi bi-plus-lg" data-bs-toggle="tooltip" data-bs-title="Clan hinzufügen"></i> <i class="bi bi-pencil-fill"></i>
</button> </button>
<button class="btn btn-lg btn-outline-secondary text-success" type="button" id="home-add"
data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#addClanModal" data-bs-action="tooltip" data-bs-title="Clan hinzufügen">
<i class="bi bi-plus-lg"></i>
</button>
{{ end }}
</div> </div>
</div> </div>
</div> </div>
@@ -26,6 +31,11 @@
<script lang="javascript"> <script lang="javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
setupClanButtons('home-clan', 'home-delete', 'home-edit'); setupClanButtons('home-clan', 'home-delete', 'home-edit');
const homePlayerList = document.getElementById('home-player-list');
homePlayerList.addEventListener('htmx:afterSwap', function() {
initTooltips(homePlayerList);
});
}); });
</script> </script>

View File

@@ -2,25 +2,28 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div id="home-player-list" class="border rounded p-1 overflow-auto" style="height: 47vh;" oninput="updateTooltips(this)"> <div id="home-player-list" class="border border-secondary rounded p-1 overflow-auto" style="height: 47vh;">
</div> </div>
</div> </div>
<div class="col-auto ps-0"> <div class="col-auto ps-0">
<div class="btn-group-vertical btn-group-lg" role="group"> <div class="btn-group-vertical btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('home-player-list')"> <button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('home-player-list')" data-bs-action="tooltip" data-bs-title="Alle auswählen">
<i class="bi bi-check-square fs-4" data-bs-toggle="tooltip" data-bs-title="Alle auswählen"></i> <i class="bi bi-check-square fs-4"></i>
</button> </button>
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('home-player-list')"> <button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('home-player-list')" data-bs-action="tooltip" data-bs-title="Nichts auswählen">
<i class="bi bi-square fs-4" data-bs-toggle="tooltip" data-bs-title="Nichts auswählen"></i> <i class="bi bi-square fs-4"></i>
</button> </button>
</div> </div>
{{ if not (eq .UserRole "READER") }}
<br>
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="home-player-add"
data-bs-toggle="modal" data-bs-list="#home-player-list" data-bs-target="#addPlayerModal" data-bs-action="tooltip" data-bs-title="Spieler hinzufügen" disabled>
<i class="bi bi-person-fill-add fs-4"></i>
</button>
{{ end }}
<br> <br>
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="home-player-add" data-bs-toggle="modal" data-bs-list="#home-player-list" data-bs-target="#addPlayerModal" disabled> <div class="vstack text-center border border-secondary rounded mt-2" data-bs-action="tooltip" data-bs-title="Ausgewählte Spieler">
<i class="bi bi-person-add fs-4" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen"></i>
</button>
<br>
<div class="vstack text-center border border-secondary rounded mt-2" data-bs-toggle="tooltip" data-bs-title="Ausgwählte Spieler">
<i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i> <i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i>
<span class="badge fs-5 mb-2" id="home-player-selected">0</span> <span class="badge fs-5 mb-2" id="home-player-selected">0</span>
</div> </div>

View File

@@ -1,23 +1,29 @@
{{ define "opp_clan_bar" }} {{ define "opp_clan_bar" }}
<div class="row g-2"> <div class="row g-2">
<div class="col-auto"> <div class="col-auto pe-2">
<label for="opponent-clan" class="col-form-label col-form-label-lg">Clans:</label> <label for="opponent-clan" class="col-form-label col-form-label-lg">Clans:</label>
</div> </div>
<div class="col"> <div class="col">
<div class="input-group input-group-lg mb-3"> <div class="input-group input-group-lg mb-3">
<select class="form-select form-control border-secondary" id="opponent-clan" hx-get="/players_html" hx-target="#opponent-player-list" hx-vals='js:{"clan_id": getSelectedClanId("opponent-clan")}'> <select class="form-select form-control border-secondary" id="opponent-clan" hx-get="/players_html" hx-target="#opponent-player-list" hx-vals='js:{"clan_id": getSelectedClanId("opponent-clan")}'>
<option disabled selected value>Auswählen...</option> <option disabled selected value>Auswählen...</option>
<!-- Options will be loaded dynamically -->
</select> </select>
<button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="opponent-delete" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#deleteClanModal" disabled> {{ if not (eq .UserRole "READER") }}
<i class="bi bi-trash3" data-bs-toggle="tooltip" data-bs-title="Clan löschen"></i> <button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="opponent-delete"
</button> data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#deleteClanModal" data-bs-action="tooltip" data-bs-title="Clan löschen" disabled>
<button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#editClanModal" id="opponent-edit" disabled> <i class="bi bi-trash3-fill"></i>
<i class="bi bi-pencil-fill" data-bs-toggle="tooltip" data-bs-title="Clan bearbeiten"></i> </button>
</button> <button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" id="opponent-edit"
<button class="btn btn-lg btn-outline-secondary text-success" type="button" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#addClanModal" id="opponent-add"> data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#editClanModal" data-bs-action="tooltip" data-bs-title="Clan bearbeiten" disabled>
<i class="bi bi-plus-lg" data-bs-toggle="tooltip" data-bs-title="Clan hinzufügen"></i> <i class="bi bi-pencil-fill"></i>
</button> </button>
<button class="btn btn-lg btn-outline-secondary text-success" type="button" id="opponent-add"
data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#addClanModal" data-bs-action="tooltip" data-bs-title="Clan hinzufügen">
<i class="bi bi-plus-lg"></i>
</button>
{{ end }}
</div> </div>
</div> </div>
</div> </div>
@@ -25,6 +31,11 @@
<script lang="javascript"> <script lang="javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
setupClanButtons('opponent-clan', 'opponent-delete', 'opponent-edit'); setupClanButtons('opponent-clan', 'opponent-delete', 'opponent-edit');
const oppPlayerList = document.getElementById('opponent-player-list');
oppPlayerList.addEventListener('htmx:afterSwap', function() {
initTooltips(oppPlayerList);
});
}); });
</script> </script>

View File

@@ -2,25 +2,28 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div id="opponent-player-list" class="border rounded p-1 overflow-auto" style="height: 47vh;"> <div id="opponent-player-list" class="border border-secondary rounded p-1 overflow-auto" style="height: 47vh;">
</div> </div>
</div> </div>
<div class="col-auto ps-0"> <div class="col-auto ps-0">
<div class="btn-group-vertical btn-group-lg" role="group"> <div class="btn-group-vertical btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('opponent-player-list')"> <button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('opponent-player-list')" data-bs-action="tooltip" data-bs-title="Alle auswählen">
<i class="bi bi-check-square fs-4" data-bs-toggle="tooltip" data-bs-title="Alle auswählen"></i> <i class="bi bi-check-square fs-4"></i>
</button> </button>
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('opponent-player-list')"> <button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('opponent-player-list')" data-bs-action="tooltip" data-bs-title="Nichts auswählen">
<i class="bi bi-square fs-4" data-bs-toggle="tooltip" data-bs-title="Nichts auswählen"></i> <i class="bi bi-square fs-4"></i>
</button> </button>
</div> </div>
{{ if not (eq .UserRole "READER") }}
<br>
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="opponent-player-add"
data-bs-toggle="modal" data-bs-list="#opponent-player-list" data-bs-target="#addPlayerModal" data-bs-action="tooltip" data-bs-title="Spieler hinzufügen" disabled>
<i class="bi bi-person-fill-add fs-4"></i>
</button>
{{ end }}
<br> <br>
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="opponent-player-add" data-bs-toggle="modal" data-bs-list="#opponent-player-list" data-bs-target="#addPlayerModal" disabled> <div class="vstack text-center border border-secondary rounded mt-2" data-bs-action="tooltip" data-bs-title="Ausgewählte Spieler">
<i class="bi bi-person-add fs-4" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen"></i>
</button>
<br>
<div class="vstack text-center border border-secondary rounded mt-2" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen">
<i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i> <i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i>
<span class="badge fs-5 mb-2" id="opponent-player-selected">0</span> <span class="badge fs-5 mb-2" id="opponent-player-selected">0</span>
</div> </div>

View File

@@ -4,11 +4,17 @@
{{ template "header" . }} {{ template "header" . }}
</head> </head>
<body data-bs-theme="dark" class="h-auto"> <body data-bs-theme="dark" class="h-auto">
<div class="container-xxl bg-dark mt-5 p-4 rounded-3 text-light"> <div class="container-xxl bg-dark mt-4 py-4 px-3 rounded-3 text-light">
<div class="text-secondary-emphasis border-bottom border-secondary-subtle">
<h1 class="text-center fw-bold">Infantry Skill Calculator</h1>
<p class="lead text-center fs-6 text-secondary fst-italic">By [GCG]FinalEnd4</p>
</div>
<div class="row"> <div class="row">
<!-- Home-Clan Column --> <!-- Home-Clan Column -->
<div class="col-md-6 d-flex flex-column border-end px-3 pb-4"> <div class="col-md-6 border-end border-secondary-subtle px-3 pb-4">
<h4 class="text-center mt-2 pb-3 mb-3 border-bottom"><i class="bi bi-people me-3"></i>Heim-Team</h4> <h4 class="text-center my-3 text-primary fs-3">
<i class="bi bi-people-fill me-3"></i>Heim-Team<i class="bi bi-people-fill ms-3"></i>
</h4>
<!-- Clan Selection --> <!-- Clan Selection -->
{{ template "home_clan_bar" . }} {{ template "home_clan_bar" . }}
@@ -17,8 +23,10 @@
</div> </div>
<!-- Opponent-Clan Column --> <!-- Opponent-Clan Column -->
<div class="col-md-6 d-flex flex-column px-3 pb-4"> <div class="col-md-6 px-3 pb-4">
<h4 class="text-center mt-2 pb-3 mb-3 border-bottom"><i class="bi bi-people me-3"></i>Gegner-Team</h4> <h4 class="text-center my-3 text-danger fs-3">
<i class="bi bi-people-fill me-3"></i>Gegner-Team<i class="bi bi-people-fill ms-3"></i>
</h4>
<!-- Clan Selection --> <!-- Clan Selection -->
{{ template "opp_clan_bar" . }} {{ template "opp_clan_bar" . }}
@@ -31,22 +39,20 @@
{{ template "bottom_controls" . }} {{ template "bottom_controls" . }}
</div> </div>
<!-- Delete Clan Modal --> {{ if not (eq .UserRole "READER")}}
{{ template "delete_clan" . }} {{ template "delete_clan" . }}
<!-- Add Clan Modal --> {{ template "add_clan" . }}
{{ template "add_clan" . }} {{ template "edit_clan" . }}
<!-- Edit Clan Modal -->
{{ template "edit_clan" . }}
<!-- Add Player Modal --> {{ template "add_player" . }}
{{ template "add_player" . }} {{ template "delete_player" . }}
<!-- Delete Player Modal --> {{ template "edit_player" . }}
{{ template "delete_player" . }} {{ end}}
<!-- Edit Player Modal -->
{{ template "edit_player" . }}
<!-- Settings Modal --> {{/* template "settings" . */}}
{{ template "settings" . }}
{{ template "full_calc" . }}
{{ template "single_calc" . }}
<script lang="javascript"> <script lang="javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

View File

@@ -9,15 +9,15 @@
<hr> <hr>
<form hx-post="/login" hx-target="#login-result" class="position-relative"> <form hx-post="/login" hx-target="#login-result" class="position-relative">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input class="form-control form-control-lg" type="text" id="username" name="username" placeholder="Username" required> <input class="form-control form-control-lg pb-1" type="text" id="username" name="username" placeholder="Username" required>
<label for="username">Username</label> <label for="username">Username</label>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input class="form-control form-control-lg" type="password" id="password" name="password" placeholder="Passwort" required> <input class="form-control form-control-lg pb-1" type="password" id="password" name="password" placeholder="Passwort" required>
<label for="password">Passwort</label> <label for="password">Passwort</label>
</div> </div>
<button class="btn btn-lg btn-primary mt-4" type="submit"><i class="bi bi-door-open me-3"></i>Anmelden</button> <button class="btn btn-lg btn-primary mt-4" type="submit"><i class="bi bi-door-open me-3"></i>Anmelden</button>
<a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/register" data-bs-toggle="tooltip" data-bs-title="Zur Registrierung"><i class="bi bi-key"></i></a> <a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/register" data-bs-action="tooltip" data-bs-title="Zur Registrierung"><i class="bi bi-key"></i></a>
</form> </form>
<div id="login-result" class="fs-4 mt-2"></div> <div id="login-result" class="fs-4 mt-2"></div>
</div> </div>

View File

@@ -1,7 +1,7 @@
{{ define "add_clan" }} {{ define "add_clan" }}
<div class="modal fade" id="addClanModal" tabindex="-1"> <div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="addClanModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -10,11 +10,11 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" class="form-control form-control-lg" id="clanName" placeholder="Clan-Name" minlength="2" maxlength="30" required> <input type="text" class="form-control form-control-lg pb-1" id="clanName" placeholder="Clan-Name" minlength="2" maxlength="30" required>
<label for="clanName">Clan-Name</label> <label for="clanName">Clan-Name</label>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control form-control-lg" id="clanTag" placeholder="Clan-Tag" minlength="2" maxlength="30" required> <input type="text" class="form-control form-control-lg pb-1" id="clanTag" placeholder="Clan-Tag" minlength="2" maxlength="30" required>
<label for="clanTag">Clan-Tag</label> <label for="clanTag">Clan-Tag</label>
</div> </div>
<div class="form-check form-check-inline mt-3 fs-5"> <div class="form-check form-check-inline mt-3 fs-5">
@@ -130,6 +130,12 @@
submitButton.addEventListener('click', submitClanHandler); submitButton.addEventListener('click', submitClanHandler);
}); });
addClanModal.addEventListener('keypress', event => {
if (event.key === 'Enter') {
submitButton.click();
}
});
addClanModal.addEventListener('hidden.bs.modal', _ => { addClanModal.addEventListener('hidden.bs.modal', _ => {
submitButton.removeEventListener('click', submitClanHandler); submitButton.removeEventListener('click', submitClanHandler);

View File

@@ -1,6 +1,6 @@
{{ define "add_player" }} {{ define "add_player" }}
<div class="modal fade" id="addPlayerModal" tabindex="-1"> <div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="addPlayerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -9,12 +9,12 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" class="form-control form-control-lg" id="playerClanName" placeholder="Clan-Name" disabled> <input type="text" class="form-control form-control-lg pb-1" id="addPlayerClanName" placeholder="Clan-Name" disabled>
<label for="playerClanName">Clan-Name</label> <label for="addPlayerClanName">Clan-Name</label>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control form-control-lg" id="playerName" placeholder="Spieler-Name" required> <input type="text" class="form-control form-control-lg pb-1" id="addPlayerName" placeholder="Spieler-Name" required>
<label for="playerName">Spieler-Name</label> <label for="addPlayerName">Spieler-Name</label>
</div> </div>
<div class="error-message text-danger fs-5 badge" style="display: none;"></div> <div class="error-message text-danger fs-5 badge" style="display: none;"></div>
</div> </div>
@@ -35,8 +35,8 @@
let selectedClan = null; let selectedClan = null;
const submitButton = addPlayerModal.querySelector('button[name="submit"]'); const submitButton = addPlayerModal.querySelector('button[name="submit"]');
const playerName = addPlayerModal.querySelector('#playerName'); const playerName = addPlayerModal.querySelector('#addPlayerName');
const clanName = addPlayerModal.querySelector('#playerClanName'); const clanName = addPlayerModal.querySelector('#addPlayerClanName');
const errorDiv = addPlayerModal.querySelector('.error-message'); const errorDiv = addPlayerModal.querySelector('.error-message');
const homeClanList = document.getElementById('home-clan'); const homeClanList = document.getElementById('home-clan');
const oppClanList = document.getElementById('opponent-clan'); const oppClanList = document.getElementById('opponent-clan');
@@ -68,8 +68,12 @@
} }
}) })
.then(response => { .then(response => {
if (!response.ok) { if (response.status === 404) {
throw new Error('Hinzufügen fehlgeschlagen!\nSpielername existiert möglichweise bereits.'); throw new Error('Hinzufügen fehlgeschlagen!\nSpielername existiert nicht.');
} else if (response.status === 503 || response.status === 504) {
throw new Error('Hinzufügen fehlgeschlagen!\nTracker überlastet. Versuche es später erneut.');
} else if (!response.ok) {
throw new Error('Hinzufügen fehlgeschlagen!\nSpielername existiert möglicherweise bereits.');
} }
return response.text(); return response.text();
}) })
@@ -98,6 +102,12 @@
clanName.value = selectedClan.innerText; clanName.value = selectedClan.innerText;
}); });
addPlayerModal.addEventListener('keypress', event => {
if (event.key === 'Enter') {
submitButton.click();
}
});
addPlayerModal.addEventListener('hidden.bs.modal', _ => { addPlayerModal.addEventListener('hidden.bs.modal', _ => {
submitButton.removeEventListener('click', submitPlayerHandler); submitButton.removeEventListener('click', submitPlayerHandler);
@@ -106,6 +116,7 @@
errorDiv.innerText = ""; errorDiv.innerText = "";
errorDiv.style.display = 'none'; errorDiv.style.display = 'none';
playerName.classList.remove('is-invalid'); playerName.classList.remove('is-invalid');
initTooltips(playerList);
}); });
} }
}); });

View File

@@ -1,13 +1,13 @@
{{ define "delete_clan" }} {{ define "delete_clan" }}
<div class="modal fade" id="deleteClanModal" tabindex="-1"> <div class="modal modal-lg fade" data-bs-backdrop="static" data-bs-keyboard="false" id="deleteClanModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h1 class="modal-title fs-3 text-danger fw-bold" id="deleteClanModalLabel">Clan löschen</h1> <h1 class="modal-title fs-3 text-danger fw-bold" id="deleteClanModalLabel">Clan löschen</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body fs-5"> <div class="modal-body fs-5 text-center">
Möchtest du den Clan <span class="text-warning fw-bold" id="clan"></span> wirklich löschen? Möchtest du den Clan <span class="text-warning fw-bold" id="clan"></span> wirklich löschen?
<br><br> <br><br>
Die Aktion kann nicht rückgängig gemacht werden. Die Aktion kann nicht rückgängig gemacht werden.
@@ -24,6 +24,7 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const deleteClanModal = document.getElementById('deleteClanModal') const deleteClanModal = document.getElementById('deleteClanModal')
const deleteClanModalBS = new bootstrap.Modal('#deleteClanModal'); const deleteClanModalBS = new bootstrap.Modal('#deleteClanModal');
const submitButton = deleteClanModal.querySelector('button[name="submit"]');
if (deleteClanModal) { if (deleteClanModal) {
deleteClanModal.addEventListener('show.bs.modal', event => { deleteClanModal.addEventListener('show.bs.modal', event => {
@@ -33,7 +34,6 @@
const modalBodyInput = deleteClanModal.querySelector('#clan'); const modalBodyInput = deleteClanModal.querySelector('#clan');
modalBodyInput.innerText = selectedClan; modalBodyInput.innerText = selectedClan;
const submitButton = deleteClanModal.querySelector('button[name="submit"]');
submitButton.addEventListener('click', function () { submitButton.addEventListener('click', function () {
const clanId = parseInt(clanList.value); const clanId = parseInt(clanList.value);
@@ -69,6 +69,12 @@
}); });
}, { once: true }); }, { once: true });
}); });
deleteClanModal.addEventListener('keypress', event => {
if (event.key === 'Enter') {
submitButton.click();
}
});
} }
}); });
</script> </script>

View File

@@ -1,13 +1,13 @@
{{ define "delete_player" }} {{ define "delete_player" }}
<div class="modal fade" id="deletePlayerModal" tabindex="-1"> <div class="modal modal-lg fade" data-bs-backdrop="static" data-bs-keyboard="false" id="deletePlayerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h1 class="modal-title fs-3 text-danger fw-bold" id="deletePlayerModalLabel">Spieler löschen</h1> <h1 class="modal-title fs-3 text-danger fw-bold" id="deletePlayerModalLabel">Spieler löschen</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body fs-5"> <div class="modal-body fs-5 text-center">
Möchtest du den Spieler <span class="text-warning fw-bold" id="player"></span> wirklich löschen? Möchtest du den Spieler <span class="text-warning fw-bold" id="player"></span> wirklich löschen?
<br><br> <br><br>
Die Aktion kann nicht rückgängig gemacht werden. Die Aktion kann nicht rückgängig gemacht werden.
@@ -24,6 +24,8 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const deletePlayerModal = document.getElementById('deletePlayerModal') const deletePlayerModal = document.getElementById('deletePlayerModal')
const deletePlayerModalBS = new bootstrap.Modal('#deletePlayerModal'); const deletePlayerModalBS = new bootstrap.Modal('#deletePlayerModal');
const submitButton = deletePlayerModal.querySelector('button[name="submit"]');
if (deletePlayerModal) { if (deletePlayerModal) {
deletePlayerModal.addEventListener('show.bs.modal', event => { deletePlayerModal.addEventListener('show.bs.modal', event => {
const button = event.relatedTarget; const button = event.relatedTarget;
@@ -37,7 +39,6 @@
const homeClanList = document.getElementById('home-clan'); const homeClanList = document.getElementById('home-clan');
const oppClanList = document.getElementById('opponent-clan'); const oppClanList = document.getElementById('opponent-clan');
const submitButton = deletePlayerModal.querySelector('button[name="submit"]');
submitButton.addEventListener('click', function () { submitButton.addEventListener('click', function () {
fetch("/player/" + playerId, { fetch("/player/" + playerId, {
method: "DELETE", method: "DELETE",
@@ -59,6 +60,12 @@
}); });
}, { once: true }); }, { once: true });
}); });
deletePlayerModal.addEventListener('keypress', event => {
if (event.key === 'Enter') {
submitButton.click();
}
});
} }
}); });
</script> </script>

View File

@@ -1,6 +1,6 @@
{{ define "edit_clan" }} {{ define "edit_clan" }}
<div class="modal fade" id="editClanModal" tabindex="-1"> <div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="editClanModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -9,11 +9,11 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" class="form-control form-control-lg" id="editClanName" placeholder="Clan-Name" minlength="2" maxlength="30" required> <input type="text" class="form-control form-control-lg pb-1" id="editClanName" placeholder="Clan-Name" minlength="2" maxlength="30" required>
<label for="editClanName">Clan-Name</label> <label for="editClanName">Clan-Name</label>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control form-control-lg" id="editClanTag" placeholder="Clan-Tag" minlength="2" maxlength="30" required> <input type="text" class="form-control form-control-lg pb-1" id="editClanTag" placeholder="Clan-Tag" minlength="2" maxlength="30" required>
<label for="editClanTag">Clan-Tag</label> <label for="editClanTag">Clan-Tag</label>
</div> </div>
<div class="form-check form-check-inline mt-3 fs-5"> <div class="form-check form-check-inline mt-3 fs-5">
@@ -149,6 +149,12 @@
}); });
}); });
editClanModal.addEventListener('keypress', event => {
if (event.key === 'Enter') {
submitButton.click();
}
});
editClanModal.addEventListener('hidden.bs.modal', _ => { editClanModal.addEventListener('hidden.bs.modal', _ => {
submitButton.removeEventListener('click', submitClanHandler); submitButton.removeEventListener('click', submitClanHandler);

View File

@@ -1,6 +1,6 @@
{{ define "edit_player" }} {{ define "edit_player" }}
<div class="modal fade" id="editPlayerModal" tabindex="-1"> <div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="editPlayerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -9,7 +9,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" class="form-control form-control-lg" id="editPlayerName" placeholder="Spieler-Name"> <input type="text" class="form-control form-control-lg pb-1" id="editPlayerName" placeholder="Spieler-Name">
<label for="editPlayerName">Spieler-Name</label> <label for="editPlayerName">Spieler-Name</label>
</div> </div>
<div class="error-message text-danger fs-5 badge" style="display: none;"></div> <div class="error-message text-danger fs-5 badge" style="display: none;"></div>
@@ -47,7 +47,7 @@
} }
function createSubmitPlayerHandler(modalEvent) { function createSubmitPlayerHandler(modalEvent) {
return function submitPlayerHandler(e) { return function submitPlayerHandler(_) {
if (!validateInput()) if (!validateInput())
return; return;
@@ -95,6 +95,12 @@
playerName.value = event.relatedTarget.closest('.input-group').querySelector('span').innerText; playerName.value = event.relatedTarget.closest('.input-group').querySelector('span').innerText;
}); });
editPlayerModal.addEventListener('keypress', event => {
if (event.key === 'Enter') {
submitButton.click();
}
});
editPlayerModal.addEventListener('hide.bs.modal', _ => { editPlayerModal.addEventListener('hide.bs.modal', _ => {
submitButton.removeEventListener('click', submitPlayerHandler); submitButton.removeEventListener('click', submitPlayerHandler);

View File

@@ -0,0 +1,357 @@
{{ define "full_calc" }}
<div class="modal modal-lg fade" data-bs-backdrop="static" data-bs-keyboard="false" id="fullCalcModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-3 text-secondary-emphasis fw-bold" id="fullCalcModalLabel">Infantryskill-Ergebnis</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body px-0">
<div class="row px-2">
<div class="col-6 border-end border-secondary px-1 pb-1" id="home-clan-results">
<h3 class="pb-3 text-center text-primary border-bottom border-primary-subtle fw-bold" id="home-clan-name">a</h3>
<!-- Dynamically added -->
</div>
<div class="col-6 px-1 mb-1" id="opponent-clan-results">
<h3 class="pb-3 text-center text-danger border-bottom border-danger-subtle fw-bold" id="opp-clan-name">b</h3>
<!-- Dynamically added -->
</div>
</div>
<div class="text-light-emphasis">
<hr class="mt-0">
</div>
<div class="row justify-content-center">
<div class="col-3 text-center fs-5 me-4 ps-0">
<i class="bi bi-graph-up me-2" data-bs-action="tooltip" data-bs-title="Durchschnitt Heim-Team"></i>
<span id="home-avg-score" class="text-warning fw-bold">
<i class="spinner-grow spinner-grow-sm text-secondary align-baseline mx-2" role="status"></i>
</span>
</div>
<div class="col-2 text-center fs-5">
<i class="bi bi-plus-slash-minus me-2" data-bs-action="tooltip" data-bs-title="Differenz der Durchschnitte" ></i>
<span id="diff-score" class="text-warning fw-bold">
<i class="spinner-grow spinner-grow-sm text-secondary align-baseline mx-2" role="status"></i>
</span>
</div>
<div class="col-3 text-center fs-5 ms-4 pe-0">
<i class="bi bi-graph-up me-2" data-bs-action="tooltip" data-bs-title="Durchschnitt Gegner-Team"></i>
<span id="opp-avg-score" class="text-warning fw-bold">
<i class="spinner-grow spinner-grow-sm text-secondary align-baseline mx-2" role="status"></i>
</span>
</div>
</div>
</div>
<div class="modal-footer justify-content-between">
<div class="text-center text-secondary ms-3" id="fullCalcInfo" hidden></div>
<div class="progress w-100" style="height: 30px;" id="fullCalcProgressbar" role="progressbar">
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0">Lade...</div>
</div>
<button type="submit" name="submit" class="btn btn-lg btn-outline-primary" onclick="downloadScreenshot()" data-bs-action="tooltip" data-bs-title="Als Bild herunterladen" hidden>
<i class="bi bi-download"></i>
</button>
</div>
</div>
</div>
</div>
<ul class="list-group list-group-horizontal fs-5 px-2 visually-hidden" id="result-item">
<li class="list-group-item border-0 rounded-0 border-bottom border-secondary-subtle w-75 text-light">
<i class="bi bi-question-square text-secondary me-2"></i>
<span id="username"></span>
</li>
<li class="list-group-item border-0 rounded-0 border-bottom border-secondary-subtle text-end w-25 ps-1 pe-3 text-warning">
<i class="bi bi-trophy text-warning me-2" hidden></i>
<span id="score"></span>
</li>
</ul>
<script lang="javascript">
function getSelectedPlayers(listId) {
const list = document.getElementById(listId);
const selectedPlayerNodes = list.querySelectorAll('input[type="checkbox"]:checked');
const selectedPlayers = [];
selectedPlayerNodes.forEach(p => {
p = p.parentElement.parentElement.parentElement.querySelector('span');
selectedPlayers.push([p.parentElement.getAttribute("data-id"), p.innerText]);
});
return selectedPlayers;
}
function getAndInsertScore(playerID, resultList) {
return fetch('/score/' + playerID)
.then(response => response.text())
.then(text => {
const score = resultList.querySelector('ul[data-id="' + playerID + '"] #score');
score.innerHTML = text;
if (!isNaN(parseFloat(text)))
score.previousElementSibling.hidden = false;
else
initTooltips(score);
return text;
});
}
function downloadScreenshot() {
const submitBtn = document.querySelector('#fullCalcModal button[name="submit"]');
submitBtn.hidden = true;
html2canvas(document.querySelector('#fullCalcModal .modal-content'), {
backgroundColor: null,
scale: 1
}).then(canvas => {
let link = document.createElement('a');
let date = new Date().toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }).replace(', ', '_').replace(':', '-');
link.download = 'InfantrySkill_' + date + '.png';
link.href = canvas.toDataURL();
link.click();
}).catch(error => {
alert('Fehler beim Erstellen des Screenshots: ' + error);
}).finally(_ => {
submitBtn.hidden = false;
});
}
document.addEventListener('DOMContentLoaded', function() {
const fullCalcModal = document.getElementById('fullCalcModal');
const downloadBtn = fullCalcModal.querySelector('button[name="submit"]');
const progressbar = document.getElementById('fullCalcProgressbar');
const resultItem = document.getElementById('result-item');
const homeClanResults = document.getElementById('home-clan-results');
const oppClanResults = document.getElementById('opponent-clan-results');
const homeClanList = document.getElementById('home-clan');
const oppClanList = document.getElementById('opponent-clan');
const spinnerBorder = '<i class="spinner-border spinner-border-sm text-warning align-baseline me-2" style="margin-left: 0.91rem;" role="status"></i>';
const spinnerGrow = '<i class="spinner-grow spinner-grow-sm text-secondary align-baseline mx-2" role="status"></i>';
const dash = '<i class="bi bi-dash mx-2"></i>';
const homeAvgScore = document.getElementById('home-avg-score');
const diffScore = document.getElementById('diff-score');
const oppAvgScore = document.getElementById('opp-avg-score');
const fullCalcInfo = document.getElementById('fullCalcInfo');
let calcMutex = false;
let sortMutex = false;
function addResultItem(resultList, playerID, playerName) {
let item = resultList.appendChild(resultItem.cloneNode(true));
item.querySelector('#username').innerText = playerName;
item.querySelector('#score').innerHTML = spinnerBorder;
item.setAttribute('data-id', playerID);
item.classList.remove('visually-hidden');
}
function calcStatistics() {
if (calcMutex) {
setTimeout(calcStatistics, 100);
return;
}
calcMutex = true;
let homeScores = [];
let oppScores = [];
homeClanResults.querySelectorAll('ul').forEach(e => {
let score = parseFloat(e.querySelector('#score').innerText);
if (!isNaN(score)) {
homeScores.push(score);
}
});
oppClanResults.querySelectorAll('ul').forEach(e => {
let score = parseFloat(e.querySelector('#score').innerText);
if (!isNaN(score)) {
oppScores.push(score);
}
});
homeScores.sort((a, b) => a - b);
oppScores.sort((a, b) => a - b);
let homeAvg = homeScores.reduce((a, b) => a + b, 0) / homeScores.length;
let oppAvg = oppScores.reduce((a, b) => a + b, 0) / oppScores.length;
if (!isNaN(homeAvg))
homeAvgScore.innerText = homeAvg.toFixed(2);
if (!isNaN(oppAvg))
oppAvgScore.innerText = oppAvg.toFixed(2);
if (!isNaN(homeAvg) && !isNaN(oppAvg)) {
const diff = (((homeAvg / oppAvg) - 1.0) * 100).toFixed(1);
diffScore.classList.remove('text-warning');
diffScore.classList.remove('text-success');
diffScore.classList.remove('text-danger');
if (diff > 0.0)
diffScore.classList.add('text-success');
else if (diff < 0.0)
diffScore.classList.add('text-danger');
diffScore.innerText = diff + "%";
}
calcMutex = false;
}
function sortPlayers() {
if (sortMutex) {
setTimeout(sortPlayers, 100);
return;
}
sortMutex = true;
let homePlayers = Array.from(homeClanResults.querySelectorAll('ul'));
let oppPlayers = Array.from(oppClanResults.querySelectorAll('ul'));
homePlayers.sort((a, b) => {
let aScore = parseFloat(a.querySelector('#score').innerText);
let bScore = parseFloat(b.querySelector('#score').innerText);
if (isNaN(aScore)) {
return 1;
} else if (isNaN(bScore)) {
return -1;
} else {
return bScore - aScore;
}
});
oppPlayers.sort((a, b) => {
let aScore = parseFloat(a.querySelector('#score').innerText);
let bScore = parseFloat(b.querySelector('#score').innerText);
if (isNaN(aScore)) {
return 1;
} else if (isNaN(bScore)) {
return -1;
} else {
return bScore - aScore;
}
});
homePlayers.forEach(p => homeClanResults.appendChild(p));
oppPlayers.forEach(p => oppClanResults.appendChild(p));
sortMutex = false;
}
function setSquadNumbers() {
let homePlayers = Array.from(homeClanResults.querySelectorAll('ul'));
let oppPlayers = Array.from(oppClanResults.querySelectorAll('ul'));
let playerCounter = 0;
function setSquadNumber(player, squadNum) {
player.querySelector('#username').previousElementSibling.classList.replace('bi-question-square', 'bi-' + squadNum + '-square');
}
homePlayers.forEach((p, _) => {
if (p.querySelector('#score').innerText === '' && p.querySelector('#score').innerHTML !== spinnerBorder) {
setSquadNumber(p, 'x');
} else {
let squadNum = Math.floor(playerCounter / 4) + 1;
setSquadNumber(p, squadNum);
playerCounter++;
}
});
playerCounter = 0;
oppPlayers.forEach((p, _) => {
if (p.querySelector('#score').innerText === '' && p.querySelector('#score').innerHTML !== spinnerBorder) {
setSquadNumber(p, 'x');
} else {
let squadNum = Math.floor(playerCounter / 4) + 1;
setSquadNumber(p, squadNum);
playerCounter++;
}
});
}
function cleanupStatistic(statisticField) {
if (statisticField.innerHTML.trim() === spinnerGrow)
statisticField.innerHTML = dash;
}
function updateScoreInClanList(clanListId, playerID, score) {
let clanPlayers = document.getElementById(clanListId).querySelectorAll('div[data-id]');
clanPlayers.forEach(p => {
if (p.getAttribute('data-id') === playerID) {
p.querySelector('#quickScore').innerHTML = score;
}
});
}
function getClanName(clanList) {
let clanName = clanList.options[clanList.selectedIndex].innerText;
return clanList.selectedIndex <= 0 ? dash : clanName.replace(/\[.*]/, '').trim();
}
function updateProgress(progress) {
progressbar.firstElementChild.style.width = progress + '%';
}
if (fullCalcModal) {
fullCalcModal.addEventListener('show.bs.modal', _ => {
let homePlayers = getSelectedPlayers('home-player-list');
let oppPlayers = getSelectedPlayers('opponent-player-list');
let promises = [];
let doneCounter = 0;
homeClanResults.querySelector('#home-clan-name').innerHTML = getClanName(homeClanList);
oppClanResults.querySelector('#opp-clan-name').innerHTML = getClanName(oppClanList);
homePlayers.forEach(p => {
addResultItem(homeClanResults, p[0], p[1]);
promises.push(
getAndInsertScore(p[0], homeClanResults)
.then(score => {
calcStatistics();
sortPlayers();
updateScoreInClanList('home-player-list', p[0], score);
updateProgress(100 * (++doneCounter / (homePlayers.length + oppPlayers.length)));
})
);
});
oppPlayers.forEach(p => {
addResultItem(oppClanResults, p[0], p[1]);
promises.push(
getAndInsertScore(p[0], oppClanResults)
.then(score => {
calcStatistics();
sortPlayers();
updateScoreInClanList('opponent-player-list', p[0], score);
updateProgress(100 * (++doneCounter / (homePlayers.length + oppPlayers.length)));
})
);
});
Promise.all(promises).then(_ => {
let liItems = homeClanResults.querySelectorAll('li');
if (liItems.length > 1) {
liItems[liItems.length - 1].classList.remove('border-bottom');
liItems[liItems.length - 2].classList.remove('border-bottom');
}
liItems = oppClanResults.querySelectorAll('li');
if (liItems.length > 1) {
liItems[liItems.length - 1].classList.remove('border-bottom');
liItems[liItems.length - 2].classList.remove('border-bottom');
}
setSquadNumbers();
cleanupStatistic(homeAvgScore);
cleanupStatistic(diffScore);
cleanupStatistic(oppAvgScore);
progressbar.hidden = true;
downloadBtn.hidden = false;
fullCalcInfo.hidden = false;
fullCalcInfo.innerHTML = 'Erstellt von <b>{{ .Username }}</b> am ' +
new Date().toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }) + ' Uhr';
});
});
fullCalcModal.addEventListener('hidden.bs.modal', _ => {
homeClanResults.querySelectorAll('ul').forEach(e => e.remove());
oppClanResults.querySelectorAll('ul').forEach(e => e.remove());
homeAvgScore.innerHTML = spinnerGrow;
diffScore.innerHTML = spinnerGrow;
oppAvgScore.innerHTML = spinnerGrow;
downloadBtn.hidden = true;
fullCalcInfo.hidden = true;
progressbar.hidden = false;
progressbar.firstElementChild.style.width = '0';
});
}
});
</script>
{{ end }}

View File

@@ -1,6 +1,6 @@
{{ define "settings" }} {{ define "settings" }}
<div class="modal fade" id="settingsModal" tabindex="-1"> <div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="settingsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -18,15 +18,15 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-check form-check-inline mt-3 fs-5"> <div class="form-check form-check-inline mt-3 fs-5 visually-hidden">
<input class="form-check-input" type="checkbox" id="settingsSquadColors" value="squadColors" checked> <input class="form-check-input" type="checkbox" id="settingsSquadColors" value="squadColors" checked>
<label class="form-check-label" for="settingsSquadColors">Squad-Farben aktivieren</label> <label class="form-check-label" for="settingsSquadColors">Squad-Farben aktivieren</label>
</div> </div>
<div class="form-check form-check-inline mt-3 fs-5"> <div class="form-check form-check-inline mt-3 fs-5 visually-hidden" data-bs-action="tooltip" data-bs-title="Median zusätzlich berechnen">
<input class="form-check-input" type="checkbox" id="settingsCalcMedian" value="calcMedian"> <input class="form-check-input" type="checkbox" id="settingsCalcMedian" value="calcMedian">
<label class="form-check-label" for="settingsCalcMedian">Median aktivieren</label> <label class="form-check-label" for="settingsCalcMedian">Median aktivieren</label>
</div> </div>
<div class="form-check form-check-inline mt-3 fs-5"> <div class="form-check form-check-inline mt-3 fs-5 visually-hidden">
<input class="form-check-input" type="checkbox" id="settingsUseCache" value="useCache" checked> <input class="form-check-input" type="checkbox" id="settingsUseCache" value="useCache" checked>
<label class="form-check-label" for="settingsUseCache">Infantryskill-Cache benutzen (empfohlen)</label> <label class="form-check-label" for="settingsUseCache">Infantryskill-Cache benutzen (empfohlen)</label>
</div> </div>
@@ -102,7 +102,6 @@
return response.json(); return response.json();
}) })
.then((result) => { .then((result) => {
console.log(result);
games.selectedIndex = games.querySelector(`option[value="${result['active_game_id']}"]`).index; games.selectedIndex = games.querySelector(`option[value="${result['active_game_id']}"]`).index;
squadColors.checked = result['squad_colors']; squadColors.checked = result['squad_colors'];
calcMedian.value = result['calc_median']; calcMedian.value = result['calc_median'];
@@ -112,6 +111,12 @@
alert('Fehler beim Laden der Einstellungen: ' + error.message); alert('Fehler beim Laden der Einstellungen: ' + error.message);
}); });
}); });
settingsModal.addEventListener('keypress', event => {
if (event.key === 'Enter') {
submitButton.click();
}
});
} }
}); });
</script> </script>

View File

@@ -0,0 +1,115 @@
{{ define "single_calc" }}
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="singleCalcModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-3 text-success fw-bold" id="singleCalcModalLabel">Einzel-Abfrage</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="form-floating">
<input type="text" class="form-control form-control-lg pb-1" id="singleCalcPlayerName" placeholder="Spieler-Name" required>
<label for="singleCalcPlayerName">Spieler-Name</label>
</div>
<div class="text-warning fs-5 badge mt-2" id="resultScore" style="display: none;"></div>
<div class="error-message text-danger fs-5 badge mt-2" style="display: none;"></div>
</div>
<div class="modal-footer">
<button type="submit" name="submit" class="btn btn-lg btn-primary">Berechnen</button>
<button type="button" class="btn btn-lg btn-secondary" data-bs-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
<script lang="javascript">
document.addEventListener('DOMContentLoaded', function() {
const singleCalcModal = document.getElementById('singleCalcModal');
let submitScoreHandler = null;
const submitButton = singleCalcModal.querySelector('button[name="submit"]');
const playerName = singleCalcModal.querySelector('#singleCalcPlayerName');
const errorDiv = singleCalcModal.querySelector('.error-message');
const resultScore = singleCalcModal.querySelector('#resultScore');
const spinner = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>Lade Daten...'
function validateInput() {
if (playerName.value.length < 1) {
playerName.classList.add('is-invalid');
return false;
}
playerName.classList.remove('is-invalid');
return true;
}
function createSubmitScoreHandler(_) {
return function submitScoreHandler(_) {
if (!validateInput())
return;
submitButton.innerHTML = spinner;
submitButton.disabled = true;
resultScore.style.display = 'none';
playerName.disabled = true;
fetch("/score/" + playerName.value, {
method: "POST",
body: JSON.stringify({
name: playerName.value
}),
headers: {
"Content-type": "application/json; charset=UTF-8"
}
})
.then(response => {
if (response.status === 404) {
throw new Error('Berechnung fehlgeschlagen!\nSpielername existiert nicht.');
} else if (response.status === 503 || response.status === 504) {
throw new Error('Berechnung fehlgeschlagen!\nTracker überlastet. Versuche es später erneut.');
} else if (!response.ok) {
throw new Error('Berechnung fehlgeschlagen!\nName ungültig oder Tracker nicht erreichbar.');
}
return response.text();
})
.then((response) => {
resultScore.innerText = 'Spieler hat ' + response + ' Punkte.';
resultScore.style.display = 'block';
}).catch((error) => {
errorDiv.innerText = error.message;
errorDiv.style.display = 'block';
}).finally(() => {
submitButton.innerHTML = 'Berechnen';
submitButton.disabled = false;
playerName.disabled = false;
});
}
}
if (singleCalcModal) {
singleCalcModal.addEventListener('show.bs.modal', event => {
submitScoreHandler = createSubmitScoreHandler(event);
submitButton.addEventListener('click', submitScoreHandler);
});
singleCalcModal.addEventListener('keypress', event => {
if (event.key === 'Enter') {
submitButton.click();
}
});
singleCalcModal.addEventListener('hidden.bs.modal', _ => {
submitButton.removeEventListener('click', submitScoreHandler);
playerName.value = "";
errorDiv.innerText = "";
errorDiv.style.display = 'none';
resultScore.innerText = "";
resultScore.style.display = 'none';
playerName.classList.remove('is-invalid');
});
}
});
</script>
{{ end }}

View File

@@ -1,21 +0,0 @@
<div class="input-group input-group-lg mb-1">
<div class="input-group-text py-1 px-2">
<input class="form-check-input fs-4 border-secondary mt-0" type="checkbox" value="" onchange="updateSelectedPlayers(this)">
</div>
<span class="form-control py-2 px-3" style="width: 10em">%s</span>
<div class="form-control text-center px-2 text-secondary">
<i class="bi bi-trophy me-3 text-warning"></i><span id="quickScore" class="text-secondary-emphasis">%s</span>
</div>
<button type="button" class="btn btn-outline-secondary" hx-get="/score/%s" hx-target="previous #quickScore" onclick="singleCalcSpinner(this)">
<i class="bi bi-calculator text-info"></i>
</button>
<button class="btn btn-outline-secondary text-secondary-emphasis dropdown-toggle py-1" type="button" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item text-primary fs-5" href="#" data-bs-toggle="modal" data-bs-id="%d" data-bs-target="#editPlayerModal">
<i class="bi bi-person-gear fs-4 me-2"></i>Bearbeiten
</a></li>
<li><a class="dropdown-item text-danger fs-5" href="#" data-bs-toggle="modal" data-bs-id="%d" data-bs-target="#deletePlayerModal">
<i class="bi bi-person-dash fs-4 me-2"></i>Löschen
</a></li>
</ul>
</div>

View File

@@ -9,28 +9,28 @@
<hr> <hr>
<form hx-post="/register" hx-target="#register-result" class="position-relative needs-validation" novalidate> <form hx-post="/register" hx-target="#register-result" class="position-relative needs-validation" novalidate>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input class="form-control form-control-lg" type="text" minlength="4" id="username" name="username" placeholder="Username" required> <input class="form-control form-control-lg pb-1" type="text" minlength="4" id="username" name="username" placeholder="Username" required>
<label for="username">Username</label> <label for="username">Username</label>
<div class="invalid-feedback"> <div class="invalid-feedback">
Mindestlänge: 4 Zeichen Mindestlänge: 4 Zeichen
</div> </div>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input class="form-control form-control-lg" type="password" minlength="8" id="password" name="password" placeholder="Passwort" required> <input class="form-control form-control-lg pb-1" type="password" minlength="8" maxlength="72" id="password" name="password" placeholder="Passwort" required>
<label for="password">Passwort</label> <label for="password">Passwort</label>
<div class="invalid-feedback"> <div class="invalid-feedback">
Mindestlänge: 8 Zeichen Min. 8 Zeichen, max. 72 Zeichen
</div> </div>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input class="form-control form-control-lg" type="password" minlength="32" maxlength="32" id="code" name="code" placeholder="Aktivierungscode" required> <input class="form-control form-control-lg pb-1" type="password" minlength="32" maxlength="32" id="code" name="code" placeholder="Aktivierungscode" required>
<label for="code">Aktivierungscode</label> <label for="code">Aktivierungscode</label>
<div class="invalid-feedback"> <div class="invalid-feedback">
Ungültiges Format Ungültiges Format
</div> </div>
</div> </div>
<button class="btn btn-lg btn-primary mt-4" type="submit"><i class="bi bi-key me-3"></i>Registrieren</button> <button class="btn btn-lg btn-primary mt-4" type="submit"><i class="bi bi-key me-3"></i>Registrieren</button>
<a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/login" data-bs-toggle="tooltip" data-bs-title="Zurück zur Anmeldung"><i class="bi bi-door-open"></i></a> <a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/login" data-bs-action="tooltip" data-bs-title="Zurück zur Anmeldung"><i class="bi bi-door-open"></i></a>
</form> </form>
<div id="register-result" class="fs-4 mt-2"></div> <div id="register-result" class="fs-4 mt-2"></div>
</div> </div>

View File

@@ -0,0 +1,27 @@
{{ range . }}
<div class="input-group input-group-lg mb-1" data-id="{{ .PlayerID }}">
<div class="input-group-text py-1 px-2">
<label>
<input class="form-check-input fs-4 border-secondary mt-1" type="checkbox" value="" onchange="updateSelectedPlayers(this)">
</label>
</div>
<span class="form-control py-2 px-3" style="width: 10em">{{ .PlayerName }}</span>
<div class="form-control text-center px-2 text-secondary border-start-0">
<i class="bi bi-trophy me-3 text-warning"></i><span id="quickScore" class="text-secondary-emphasis">{{ .Score }}</span>
</div>
<button type="button" class="btn btn-outline-secondary" hx-get="/score/{{ .PlayerID }}" hx-target="previous #quickScore" onclick="singleCalcSpinner(this)" data-bs-action="tooltip" data-bs-title="Schnell-Berechnung">
<i class="bi bi-calculator-fill text-warning"></i>
</button>
{{ if not (eq .UserRole "READER") }}
<button class="btn btn-outline-secondary text-secondary-emphasis dropdown-toggle py-1" type="button" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li><button class="dropdown-item text-primary fs-5" data-bs-toggle="modal" data-bs-id="{{ .PlayerID }}" data-bs-target="#editPlayerModal">
<i class="bi bi-person-fill-gear fs-4 me-2"></i>Bearbeiten
</button></li>
<li><button class="dropdown-item text-danger fs-5" data-bs-toggle="modal" data-bs-id="{{ .PlayerID }}" data-bs-target="#deletePlayerModal">
<i class="bi bi-person-fill-dash fs-4 me-2"></i>Löschen
</button></li>
</ul>
{{ end }}
</div>
{{ end }}

View File

@@ -10,7 +10,11 @@ func UintToString(val uint) string {
} }
func StringToUint(val string) uint { func StringToUint(val string) uint {
res, _ := strconv.ParseUint(val, 10, 16) res, err := strconv.ParseUint(val, 10, 16)
if err != nil {
Logger.Warnf("StringToUint error for %s: %s", val, err.Error())
return 0
}
return uint(res) return uint(res)
} }
@@ -19,6 +23,10 @@ func FloatToString(val float32) string {
} }
func StringToFloat(val string) float32 { func StringToFloat(val string) float32 {
res, _ := strconv.ParseFloat(val, 32) res, err := strconv.ParseFloat(val, 32)
if err != nil {
Logger.Warnf("StringToFloat error for %s: %s", val, err.Error())
return 0
}
return float32(res) return float32(res)
} }

View File

@@ -1,11 +1,15 @@
package utils package utils
import ( import (
"InfantrySkillCalculator/models" "github.com/sirupsen/logrus"
"io" "html/template"
"time" "time"
) )
var GinWriter io.Writer = nil var Logger *logrus.Logger
var GameMetrics models.GameMetrics
var PlayerCacheLifetime = 24 * time.Hour var PlayerCacheLifetime = 24 * time.Hour
var MainPageTemplates *template.Template
var LoginPageTemplates *template.Template
var RegisterPageTemplates *template.Template
var PlayerItemTemplate *template.Template

View File

@@ -3,12 +3,30 @@ package utils
import ( import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"io"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/flate"
"github.com/klauspost/compress/gzip"
) )
func GenerateActivationCode() string { func GenerateActivationCode() string {
bytes := make([]byte, 16) bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
panic(err) // Handle the error appropriately in production Logger.Fatalf("Error generating activation code: %v", err)
} }
return hex.EncodeToString(bytes) return hex.EncodeToString(bytes)
} }
func DecompressResponseBody(encoding string, body io.Reader) (io.ReadCloser, error) {
switch encoding {
case "gzip":
return gzip.NewReader(body)
case "deflate":
return flate.NewReader(body), nil
case "br":
return io.NopCloser(brotli.NewReader(body)), nil
default:
return io.NopCloser(body), nil
}
}

View File

@@ -1,129 +0,0 @@
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)
}