Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
760412d7db | ||
|
|
4ab94de529 | ||
|
|
b3f89593fb | ||
|
|
2a863c51e9 | ||
|
|
a70d923a6a | ||
|
|
f660511b63 | ||
|
|
044af60699 | ||
|
|
384036bcdd | ||
|
|
ef300d233b | ||
|
|
edad65d6a9 | ||
|
|
486c972bba |
93
cmd/steam-crypt/main.go
Normal file
93
cmd/steam-crypt/main.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/utl/configs"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
encrypt = flag.Bool("encrypt", false, "Encrypt a password")
|
||||||
|
decrypt = flag.Bool("decrypt", false, "Decrypt a password")
|
||||||
|
password = flag.String("password", "", "Password to encrypt/decrypt")
|
||||||
|
help = flag.Bool("help", false, "Show help")
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *help || (!*encrypt && !*decrypt) {
|
||||||
|
showHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *encrypt && *decrypt {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: Cannot specify both -encrypt and -decrypt\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *password == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: Password is required\n")
|
||||||
|
showHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize configs to load encryption key
|
||||||
|
configs.Init()
|
||||||
|
|
||||||
|
if *encrypt {
|
||||||
|
encrypted, err := model.EncryptPassword(*password)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error encrypting password: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *decrypt {
|
||||||
|
decrypted, err := model.DecryptPassword(*password)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error decrypting password: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(decrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showHelp() {
|
||||||
|
fmt.Println("Steam Credentials Encryption/Decryption Utility")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("This utility encrypts and decrypts Steam credentials using the same")
|
||||||
|
fmt.Println("AES-256-GCM encryption used by the ACC Server Manager application.")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Usage:")
|
||||||
|
fmt.Println(" steam-crypt -encrypt -password \"your_password\"")
|
||||||
|
fmt.Println(" steam-crypt -decrypt -password \"encrypted_string\"")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Options:")
|
||||||
|
fmt.Println(" -encrypt Encrypt the provided password")
|
||||||
|
fmt.Println(" -decrypt Decrypt the provided encrypted string")
|
||||||
|
fmt.Println(" -password The password to encrypt or encrypted string to decrypt")
|
||||||
|
fmt.Println(" -help Show this help message")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Environment Variables Required:")
|
||||||
|
fmt.Println(" ENCRYPTION_KEY - 32-byte encryption key (same as main application)")
|
||||||
|
fmt.Println(" APP_SECRET - Application secret (required by configs)")
|
||||||
|
fmt.Println(" APP_SECRET_CODE - Application secret code (required by configs)")
|
||||||
|
fmt.Println(" ACCESS_KEY - Access key (required by configs)")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Examples:")
|
||||||
|
fmt.Println(" # Encrypt a password")
|
||||||
|
fmt.Println(" steam-crypt -encrypt -password \"mysteampassword\"")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(" # Decrypt an encrypted password")
|
||||||
|
fmt.Println(" steam-crypt -decrypt -password \"base64encryptedstring\"")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Security Notes:")
|
||||||
|
fmt.Println(" - The encryption key must be exactly 32 bytes for AES-256")
|
||||||
|
fmt.Println(" - Uses AES-256-GCM for authenticated encryption")
|
||||||
|
fmt.Println(" - Each encryption includes a unique nonce for security")
|
||||||
|
fmt.Println(" - Passwords are validated for length and basic security")
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ func NewMembershipController(service *service.MembershipService, auth *middlewar
|
|||||||
}
|
}
|
||||||
|
|
||||||
routeGroups.Auth.Post("/login", mc.Login)
|
routeGroups.Auth.Post("/login", mc.Login)
|
||||||
routeGroups.Auth.Post("/open-token", mc.GenerateOpenToken)
|
routeGroups.Auth.Post("/open-token", mc.auth.Authenticate, mc.GenerateOpenToken)
|
||||||
|
|
||||||
usersGroup := routeGroups.Membership
|
usersGroup := routeGroups.Membership
|
||||||
usersGroup.Use(mc.auth.Authenticate)
|
usersGroup.Use(mc.auth.Authenticate)
|
||||||
@@ -95,7 +95,7 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
|||||||
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
|
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
|
||||||
// @Router /auth/open-token [post]
|
// @Router /auth/open-token [post]
|
||||||
func (c *MembershipController) GenerateOpenToken(ctx *fiber.Ctx) error {
|
func (c *MembershipController) GenerateOpenToken(ctx *fiber.Ctx) error {
|
||||||
token, err := c.service.GenerateOpenToken(ctx.UserContext(), ctx.Locals("userId").(string))
|
token, err := c.service.GenerateOpenToken(ctx.UserContext(), ctx.Locals("userID").(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.errorHandler.HandleAuthError(ctx, err)
|
return c.errorHandler.HandleAuthError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,20 +52,28 @@ func NewAuthMiddleware(ms *service.MembershipService, cache *cache.InMemoryCache
|
|||||||
|
|
||||||
// Authenticate is a middleware for JWT authentication with enhanced security.
|
// Authenticate is a middleware for JWT authentication with enhanced security.
|
||||||
func (m *AuthMiddleware) AuthenticateOpen(ctx *fiber.Ctx) error {
|
func (m *AuthMiddleware) AuthenticateOpen(ctx *fiber.Ctx) error {
|
||||||
return m.AuthenticateWithHandler(m.openJWTHandler.JWTHandler, ctx)
|
return m.AuthenticateWithHandler(m.openJWTHandler.JWTHandler, true, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate is a middleware for JWT authentication with enhanced security.
|
// Authenticate is a middleware for JWT authentication with enhanced security.
|
||||||
func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error {
|
func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error {
|
||||||
return m.AuthenticateWithHandler(m.jwtHandler, ctx)
|
return m.AuthenticateWithHandler(m.jwtHandler, false, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AuthMiddleware) AuthenticateWithHandler(jwtHandler *jwt.JWTHandler, ctx *fiber.Ctx) error {
|
func (m *AuthMiddleware) AuthenticateWithHandler(jwtHandler *jwt.JWTHandler, isOpenToken bool, ctx *fiber.Ctx) error {
|
||||||
// Log authentication attempt
|
// Log authentication attempt
|
||||||
ip := ctx.IP()
|
ip := ctx.IP()
|
||||||
userAgent := ctx.Get("User-Agent")
|
userAgent := ctx.Get("User-Agent")
|
||||||
|
|
||||||
authHeader := ctx.Get("Authorization")
|
authHeader := ctx.Get("Authorization")
|
||||||
|
|
||||||
|
if jwtHandler.IsOpenToken && !isOpenToken {
|
||||||
|
logging.Error("Authentication failed: attempting to authenticate with open token")
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "Wrong token type used",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
logging.Error("Authentication failed: missing Authorization header from IP %s", ip)
|
logging.Error("Authentication failed: missing Authorization header from IP %s", ip)
|
||||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
@@ -98,6 +106,13 @@ func (m *AuthMiddleware) AuthenticateWithHandler(jwtHandler *jwt.JWTHandler, ctx
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !jwtHandler.IsOpenToken && claims.IsOpenToken {
|
||||||
|
logging.Error("Authentication failed: attempting to authenticate with open token")
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "Wrong token type used",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Additional security: validate user ID format
|
// Additional security: validate user ID format
|
||||||
if claims.UserID == "" || len(claims.UserID) < 10 {
|
if claims.UserID == "" || len(claims.UserID) < 10 {
|
||||||
logging.Error("Authentication failed: invalid user ID in token from IP %s", ip)
|
logging.Error("Authentication failed: invalid user ID in token from IP %s", ip)
|
||||||
|
|||||||
124
local/migrations/003_update_state_history_sessions.go
Normal file
124
local/migrations/003_update_state_history_sessions.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateStateHistorySessions migrates tables from integer IDs to UUIDs
|
||||||
|
type UpdateStateHistorySessions struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUpdateStateHistorySessions creates a new UUID migration
|
||||||
|
func NewUpdateStateHistorySessions(db *gorm.DB) *UpdateStateHistorySessions {
|
||||||
|
return &UpdateStateHistorySessions{DB: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up executes the migration
|
||||||
|
func (m *UpdateStateHistorySessions) Up() error {
|
||||||
|
logging.Info("Checking UUID migration...")
|
||||||
|
|
||||||
|
// Check if migration is needed by looking at the servers table structure
|
||||||
|
if !m.needsMigration() {
|
||||||
|
logging.Info("UUID migration not needed - tables already use UUID primary keys")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Starting UUID migration...")
|
||||||
|
|
||||||
|
// Check if migration has already been applied
|
||||||
|
var migrationRecord MigrationRecord
|
||||||
|
err := m.DB.Where("migration_name = ?", "002_migrate_to_uuid").First(&migrationRecord).Error
|
||||||
|
if err == nil {
|
||||||
|
logging.Info("UUID migration already applied, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create migration tracking table if it doesn't exist
|
||||||
|
if err := m.DB.AutoMigrate(&MigrationRecord{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration tracking table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the UUID migration using the existing migration function
|
||||||
|
logging.Info("Executing UUID migration...")
|
||||||
|
if err := runUUIDMigrationSQL(m.DB); err != nil {
|
||||||
|
return fmt.Errorf("failed to execute UUID migration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("UUID migration completed successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// needsMigration checks if the UUID migration is needed by examining table structure
|
||||||
|
func (m *UpdateStateHistorySessions) needsMigration() bool {
|
||||||
|
// Check if servers table exists and has integer primary key
|
||||||
|
var result struct {
|
||||||
|
Exists bool `gorm:"column:exists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.DB.Raw(`
|
||||||
|
SELECT count(*) > 0 as exists FROM state_history
|
||||||
|
WHERE length(session) > 1 LIMIT 1;
|
||||||
|
`).Scan(&result).Error
|
||||||
|
|
||||||
|
if err != nil || !result.Exists {
|
||||||
|
// Table doesn't exist or no primary key found - assume no migration needed
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return result.Exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Down reverses the migration (not implemented for safety)
|
||||||
|
func (m *UpdateStateHistorySessions) Down() error {
|
||||||
|
logging.Error("UUID migration rollback is not supported for data safety reasons")
|
||||||
|
return fmt.Errorf("UUID migration rollback is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runUpdateStateHistorySessionsMigration executes the UUID migration using the SQL file
|
||||||
|
func runUpdateStateHistorySessionsMigration(db *gorm.DB) error {
|
||||||
|
// Disable foreign key constraints during migration
|
||||||
|
if err := db.Exec("PRAGMA foreign_keys=OFF").Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to disable foreign keys: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
tx := db.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %v", tx.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
migrationSQL := "UPDATE state_history SET session = upper(substr(session, 1, 1));"
|
||||||
|
|
||||||
|
// Execute the migration
|
||||||
|
if err := tx.Exec(string(migrationSQL)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("failed to execute migration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to commit migration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable foreign key constraints
|
||||||
|
if err := db.Exec("PRAGMA foreign_keys=ON").Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to re-enable foreign keys: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunUpdateStateHistorySessionsMigration is a convenience function to run the migration
|
||||||
|
func RunUpdateStateHistorySessionsMigration(db *gorm.DB) error {
|
||||||
|
migration := NewUpdateStateHistorySessions(db)
|
||||||
|
return migration.Up()
|
||||||
|
}
|
||||||
@@ -79,11 +79,11 @@ type EventConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
HourOfDay IntString `json:"hourOfDay"`
|
HourOfDay IntString `json:"hourOfDay"`
|
||||||
DayOfWeekend IntString `json:"dayOfWeekend"`
|
DayOfWeekend IntString `json:"dayOfWeekend"`
|
||||||
TimeMultiplier IntString `json:"timeMultiplier"`
|
TimeMultiplier IntString `json:"timeMultiplier"`
|
||||||
SessionType string `json:"sessionType"`
|
SessionType TrackSession `json:"sessionType"`
|
||||||
SessionDurationMinutes IntString `json:"sessionDurationMinutes"`
|
SessionDurationMinutes IntString `json:"sessionDurationMinutes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssistRules struct {
|
type AssistRules struct {
|
||||||
|
|||||||
@@ -73,12 +73,12 @@ type State struct {
|
|||||||
|
|
||||||
type ServerState struct {
|
type ServerState struct {
|
||||||
sync.RWMutex `swaggerignore:"-" json:"-"`
|
sync.RWMutex `swaggerignore:"-" json:"-"`
|
||||||
Session string `json:"session"`
|
Session TrackSession `json:"session"`
|
||||||
SessionStart time.Time `json:"sessionStart"`
|
SessionStart time.Time `json:"sessionStart"`
|
||||||
PlayerCount int `json:"playerCount"`
|
PlayerCount int `json:"playerCount"`
|
||||||
Track string `json:"track"`
|
Track string `json:"track"`
|
||||||
MaxConnections int `json:"maxConnections"`
|
MaxConnections int `json:"maxConnections"`
|
||||||
SessionDurationMinutes int `json:"sessionDurationMinutes"`
|
SessionDurationMinutes int `json:"sessionDurationMinutes"`
|
||||||
// Players map[int]*PlayerState
|
// Players map[int]*PlayerState
|
||||||
// etc.
|
// etc.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -13,9 +16,9 @@ type StateHistoryFilter struct {
|
|||||||
DateRangeFilter // Adds date range filtering
|
DateRangeFilter // Adds date range filtering
|
||||||
|
|
||||||
// Additional fields specific to state history
|
// Additional fields specific to state history
|
||||||
Session string `query:"session"`
|
Session TrackSession `query:"session"`
|
||||||
MinPlayers *int `query:"min_players"`
|
MinPlayers *int `query:"min_players"`
|
||||||
MaxPlayers *int `query:"max_players"`
|
MaxPlayers *int `query:"max_players"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyFilter implements the Filterable interface
|
// ApplyFilter implements the Filterable interface
|
||||||
@@ -52,16 +55,66 @@ func (f *StateHistoryFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TrackSession string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SessionPractice TrackSession = "P"
|
||||||
|
SessionQualify TrackSession = "Q"
|
||||||
|
SessionRace TrackSession = "R"
|
||||||
|
SessionUnknown TrackSession = "U"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *TrackSession) UnmarshalJSON(b []byte) error {
|
||||||
|
var str string
|
||||||
|
if err := json.Unmarshal(b, &str); err == nil {
|
||||||
|
*i = ToTrackSession(str)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("invalid TrackSession value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i TrackSession) Humanize() string {
|
||||||
|
switch i {
|
||||||
|
case SessionPractice:
|
||||||
|
return "Practice"
|
||||||
|
case SessionQualify:
|
||||||
|
return "Qualifying"
|
||||||
|
case SessionRace:
|
||||||
|
return "Race"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTrackSession(i string) TrackSession {
|
||||||
|
sessionAbrv := strings.ToUpper(i[:1])
|
||||||
|
switch sessionAbrv {
|
||||||
|
case "P":
|
||||||
|
return SessionPractice
|
||||||
|
case "Q":
|
||||||
|
return SessionQualify
|
||||||
|
case "R":
|
||||||
|
return SessionRace
|
||||||
|
default:
|
||||||
|
return SessionUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i TrackSession) ToString() string {
|
||||||
|
return string(i)
|
||||||
|
}
|
||||||
|
|
||||||
type StateHistory struct {
|
type StateHistory struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid"`
|
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid"`
|
||||||
Session string `json:"session"`
|
Session TrackSession `json:"session"`
|
||||||
Track string `json:"track"`
|
Track string `json:"track"`
|
||||||
PlayerCount int `json:"playerCount"`
|
PlayerCount int `json:"playerCount"`
|
||||||
DateCreated time.Time `json:"dateCreated"`
|
DateCreated time.Time `json:"dateCreated"`
|
||||||
SessionStart time.Time `json:"sessionStart"`
|
SessionStart time.Time `json:"sessionStart"`
|
||||||
SessionDurationMinutes int `json:"sessionDurationMinutes"`
|
SessionDurationMinutes int `json:"sessionDurationMinutes"`
|
||||||
SessionID uuid.UUID `json:"sessionId" gorm:"not null;type:uuid"` // Unique identifier for each session/event
|
SessionID uuid.UUID `json:"sessionId" gorm:"not null;type:uuid"` // Unique identifier for each session/event
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate is a GORM hook that runs before creating new state history entries
|
// BeforeCreate is a GORM hook that runs before creating new state history entries
|
||||||
|
|||||||
@@ -1,36 +1,38 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
type SessionCount struct {
|
type SessionCount struct {
|
||||||
Name string `json:"name"`
|
Name TrackSession `json:"name"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailyActivity struct {
|
type DailyActivity struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
SessionsCount int `json:"sessionsCount"`
|
SessionsCount int `json:"sessionsCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerCountPoint struct {
|
type PlayerCountPoint struct {
|
||||||
Timestamp string `json:"timestamp"`
|
Timestamp string `json:"timestamp"`
|
||||||
Count float64 `json:"count"`
|
Count float64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateHistoryStats struct {
|
type StateHistoryStats struct {
|
||||||
AveragePlayers float64 `json:"averagePlayers"`
|
AveragePlayers float64 `json:"averagePlayers"`
|
||||||
PeakPlayers int `json:"peakPlayers"`
|
PeakPlayers int `json:"peakPlayers"`
|
||||||
TotalSessions int `json:"totalSessions"`
|
TotalSessions int `json:"totalSessions"`
|
||||||
TotalPlaytime int `json:"totalPlaytime" gorm:"-"` // in minutes
|
TotalPlaytime int `json:"totalPlaytime" gorm:"-"` // in minutes
|
||||||
PlayerCountOverTime []PlayerCountPoint `json:"playerCountOverTime" gorm:"-"`
|
PlayerCountOverTime []PlayerCountPoint `json:"playerCountOverTime" gorm:"-"`
|
||||||
SessionTypes []SessionCount `json:"sessionTypes" gorm:"-"`
|
SessionTypes []SessionCount `json:"sessionTypes" gorm:"-"`
|
||||||
DailyActivity []DailyActivity `json:"dailyActivity" gorm:"-"`
|
DailyActivity []DailyActivity `json:"dailyActivity" gorm:"-"`
|
||||||
RecentSessions []RecentSession `json:"recentSessions" gorm:"-"`
|
RecentSessions []RecentSession `json:"recentSessions" gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecentSession struct {
|
type RecentSession struct {
|
||||||
ID uint `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
Type string `json:"type"`
|
Type TrackSession `json:"type"`
|
||||||
Track string `json:"track"`
|
Track string `json:"track"`
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
Players int `json:"players"`
|
Players int `json:"players"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ func (r *StateHistoryRepository) GetRecentSessions(ctx context.Context, filter *
|
|||||||
FROM state_histories
|
FROM state_histories
|
||||||
WHERE server_id = ? AND date_created BETWEEN ? AND ?
|
WHERE server_id = ? AND date_created BETWEEN ? AND ?
|
||||||
GROUP BY session_id
|
GROUP BY session_id
|
||||||
HAVING COUNT(*) > 1 AND MAX(player_count) > 0
|
HAVING MAX(player_count) > 0
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -63,16 +63,11 @@ func (s *MembershipService) Login(ctx context.Context, username, password string
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.jwtHandler.GenerateToken(user)
|
return s.jwtHandler.GenerateToken(user.ID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MembershipService) GenerateOpenToken(ctx context.Context, userId string) (string, error) {
|
func (s *MembershipService) GenerateOpenToken(ctx context.Context, userId string) (string, error) {
|
||||||
user, err := s.repo.GetByID(ctx, userId)
|
return s.openJwtHandler.GenerateToken(userId)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.openJwtHandler.GenerateToken(user)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user.
|
// CreateUser creates a new user.
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func (s *ServerService) insertStateHistory(serverID uuid.UUID, state *model.Serv
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) updateSessionDuration(server *model.Server, sessionType string) {
|
func (s *ServerService) updateSessionDuration(server *model.Server, sessionType model.TrackSession) {
|
||||||
// Get configs using helper methods
|
// Get configs using helper methods
|
||||||
event, err := s.configService.GetEventConfig(server)
|
event, err := s.configService.GetEventConfig(server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -124,33 +124,44 @@ func (s *SteamService) InstallServer(ctx context.Context, installPath string, se
|
|||||||
// Get SteamCMD path from environment variable
|
// Get SteamCMD path from environment variable
|
||||||
steamCMDPath := env.GetSteamCMDPath()
|
steamCMDPath := env.GetSteamCMDPath()
|
||||||
|
|
||||||
// Build SteamCMD command
|
// Build SteamCMD command arguments
|
||||||
args := []string{
|
steamCMDArgs := []string{
|
||||||
"-nologo",
|
|
||||||
"-noprofile",
|
|
||||||
steamCMDPath,
|
|
||||||
"+force_install_dir", absPath,
|
"+force_install_dir", absPath,
|
||||||
"+login",
|
"+login",
|
||||||
}
|
}
|
||||||
|
|
||||||
if creds != nil && creds.Username != "" {
|
if creds != nil && creds.Username != "" {
|
||||||
args = append(args, creds.Username)
|
steamCMDArgs = append(steamCMDArgs, creds.Username)
|
||||||
if creds.Password != "" {
|
if creds.Password != "" {
|
||||||
args = append(args, creds.Password)
|
steamCMDArgs = append(steamCMDArgs, creds.Password)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
args = append(args, "anonymous")
|
steamCMDArgs = append(steamCMDArgs, "anonymous")
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args,
|
steamCMDArgs = append(steamCMDArgs,
|
||||||
"+app_update", ACCServerAppID,
|
"+app_update", ACCServerAppID,
|
||||||
"validate",
|
"validate",
|
||||||
"+quit",
|
"+quit",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use interactive executor to handle potential 2FA prompts
|
// Build PowerShell arguments to execute SteamCMD directly
|
||||||
|
// This matches the format: powershell -nologo -noprofile c:\steamcmd\steamcmd.exe +args...
|
||||||
|
args := []string{"-nologo", "-noprofile"}
|
||||||
|
args = append(args, steamCMDPath)
|
||||||
|
args = append(args, steamCMDArgs...)
|
||||||
|
|
||||||
|
// Use interactive executor to handle potential 2FA prompts with timeout
|
||||||
logging.Info("Installing ACC server to %s...", absPath)
|
logging.Info("Installing ACC server to %s...", absPath)
|
||||||
if err := s.interactiveExecutor.ExecuteInteractive(ctx, serverID, args...); err != nil {
|
|
||||||
|
// Create a context with timeout to prevent hanging indefinitely
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := s.interactiveExecutor.ExecuteInteractive(timeoutCtx, serverID, args...); err != nil {
|
||||||
|
if timeoutCtx.Err() == context.DeadlineExceeded {
|
||||||
|
return fmt.Errorf("SteamCMD operation timed out after 10 minutes")
|
||||||
|
}
|
||||||
return fmt.Errorf("failed to run SteamCMD: %v", err)
|
return fmt.Errorf("failed to run SteamCMD: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,14 +61,33 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create channels for output monitoring
|
// Create channels for output monitoring
|
||||||
outputDone := make(chan error)
|
outputDone := make(chan error, 1)
|
||||||
|
cmdDone := make(chan error, 1)
|
||||||
|
|
||||||
// Monitor stdout and stderr for 2FA prompts
|
// Monitor stdout and stderr for 2FA prompts
|
||||||
go e.monitorOutput(ctx, stdout, stderr, serverID, outputDone)
|
go e.monitorOutput(ctx, stdout, stderr, serverID, outputDone)
|
||||||
|
|
||||||
// Wait for either the command to finish or output monitoring to complete
|
// Wait for the command to finish in a separate goroutine
|
||||||
cmdErr := cmd.Wait()
|
go func() {
|
||||||
outputErr := <-outputDone
|
cmdDone <- cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for both command and output monitoring to complete
|
||||||
|
var cmdErr, outputErr error
|
||||||
|
completedCount := 0
|
||||||
|
|
||||||
|
for completedCount < 2 {
|
||||||
|
select {
|
||||||
|
case cmdErr = <-cmdDone:
|
||||||
|
completedCount++
|
||||||
|
logging.Info("Command execution completed")
|
||||||
|
case outputErr = <-outputDone:
|
||||||
|
completedCount++
|
||||||
|
logging.Info("Output monitoring completed")
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if outputErr != nil {
|
if outputErr != nil {
|
||||||
logging.Warn("Output monitoring error: %v", outputErr)
|
logging.Warn("Output monitoring error: %v", outputErr)
|
||||||
@@ -78,45 +97,85 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) {
|
func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) {
|
||||||
defer close(done)
|
defer func() {
|
||||||
|
select {
|
||||||
|
case done <- nil:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Create scanners for both outputs
|
// Create scanners for both outputs
|
||||||
stdoutScanner := bufio.NewScanner(stdout)
|
stdoutScanner := bufio.NewScanner(stdout)
|
||||||
stderrScanner := bufio.NewScanner(stderr)
|
stderrScanner := bufio.NewScanner(stderr)
|
||||||
|
|
||||||
outputChan := make(chan string)
|
outputChan := make(chan string, 100) // Buffered channel to prevent blocking
|
||||||
|
readersDone := make(chan struct{}, 2)
|
||||||
|
|
||||||
// Read from stdout
|
// Read from stdout
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() { readersDone <- struct{}{} }()
|
||||||
for stdoutScanner.Scan() {
|
for stdoutScanner.Scan() {
|
||||||
line := stdoutScanner.Text()
|
line := stdoutScanner.Text()
|
||||||
if e.LogOutput {
|
if e.LogOutput {
|
||||||
logging.Info("STDOUT: %s", line)
|
logging.Info("STDOUT: %s", line)
|
||||||
}
|
}
|
||||||
outputChan <- line
|
select {
|
||||||
|
case outputChan <- line:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := stdoutScanner.Err(); err != nil {
|
||||||
|
logging.Warn("Stdout scanner error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Read from stderr
|
// Read from stderr
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() { readersDone <- struct{}{} }()
|
||||||
for stderrScanner.Scan() {
|
for stderrScanner.Scan() {
|
||||||
line := stderrScanner.Text()
|
line := stderrScanner.Text()
|
||||||
if e.LogOutput {
|
if e.LogOutput {
|
||||||
logging.Info("STDERR: %s", line)
|
logging.Info("STDERR: %s", line)
|
||||||
}
|
}
|
||||||
outputChan <- line
|
select {
|
||||||
|
case outputChan <- line:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := stderrScanner.Err(); err != nil {
|
||||||
|
logging.Warn("Stderr scanner error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Monitor for 2FA prompts
|
// Monitor for completion and 2FA prompts
|
||||||
|
readersFinished := 0
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
done <- ctx.Err()
|
done <- ctx.Err()
|
||||||
return
|
return
|
||||||
|
case <-readersDone:
|
||||||
|
readersFinished++
|
||||||
|
if readersFinished == 2 {
|
||||||
|
// Both readers are done, close output channel and finish monitoring
|
||||||
|
close(outputChan)
|
||||||
|
// Drain any remaining output
|
||||||
|
for line := range outputChan {
|
||||||
|
if e.is2FAPrompt(line) {
|
||||||
|
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
|
||||||
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
|
done <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
case line, ok := <-outputChan:
|
case line, ok := <-outputChan:
|
||||||
if !ok {
|
if !ok {
|
||||||
done <- nil
|
// Channel closed, we're done
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "0.10.5"
|
Version = "0.10.7"
|
||||||
Prefix = "v1"
|
Prefix = "v1"
|
||||||
Secret string
|
Secret string
|
||||||
SecretCode string
|
SecretCode string
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import (
|
|||||||
|
|
||||||
// Claims represents the JWT claims.
|
// Claims represents the JWT claims.
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
|
IsOpenToken bool `json:"is_open_token"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTHandler struct {
|
type JWTHandler struct {
|
||||||
SecretKey []byte
|
SecretKey []byte
|
||||||
|
IsOpenToken bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenJWTHandler struct {
|
type OpenJWTHandler struct {
|
||||||
@@ -28,6 +30,7 @@ type OpenJWTHandler struct {
|
|||||||
// NewJWTHandler creates a new JWTHandler instance with the provided secret key.
|
// NewJWTHandler creates a new JWTHandler instance with the provided secret key.
|
||||||
func NewOpenJWTHandler(jwtSecret string) *OpenJWTHandler {
|
func NewOpenJWTHandler(jwtSecret string) *OpenJWTHandler {
|
||||||
jwtHandler := NewJWTHandler(jwtSecret)
|
jwtHandler := NewJWTHandler(jwtSecret)
|
||||||
|
jwtHandler.IsOpenToken = true
|
||||||
return &OpenJWTHandler{
|
return &OpenJWTHandler{
|
||||||
JWTHandler: jwtHandler,
|
JWTHandler: jwtHandler,
|
||||||
}
|
}
|
||||||
@@ -68,13 +71,14 @@ func (jh *JWTHandler) GenerateSecretKey() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateToken generates a new JWT for a given user.
|
// GenerateToken generates a new JWT for a given user.
|
||||||
func (jh *JWTHandler) GenerateToken(user *model.User) (string, error) {
|
func (jh *JWTHandler) GenerateToken(userId string) (string, error) {
|
||||||
expirationTime := time.Now().Add(24 * time.Hour)
|
expirationTime := time.Now().Add(24 * time.Hour)
|
||||||
claims := &Claims{
|
claims := &Claims{
|
||||||
UserID: user.ID.String(),
|
UserID: userId,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||||
},
|
},
|
||||||
|
IsOpenToken: jh.IsOpenToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
@@ -88,6 +92,7 @@ func (jh *JWTHandler) GenerateTokenWithExpiry(user *model.User, expiry time.Time
|
|||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||||
},
|
},
|
||||||
|
IsOpenToken: jh.IsOpenToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ type PathValidator struct {
|
|||||||
func NewPathValidator() *PathValidator {
|
func NewPathValidator() *PathValidator {
|
||||||
blockedPatterns := []*regexp.Regexp{
|
blockedPatterns := []*regexp.Regexp{
|
||||||
regexp.MustCompile(`\.\.`),
|
regexp.MustCompile(`\.\.`),
|
||||||
regexp.MustCompile(`[<>:"|?*]`),
|
|
||||||
regexp.MustCompile(`^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$`),
|
regexp.MustCompile(`^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$`),
|
||||||
regexp.MustCompile(`\x00`),
|
regexp.MustCompile(`\x00`),
|
||||||
regexp.MustCompile(`^\\\\`),
|
regexp.MustCompile(`^\\\\`),
|
||||||
@@ -92,4 +91,4 @@ func (pv *PathValidator) AddAllowedBasePath(path string) error {
|
|||||||
|
|
||||||
func (pv *PathValidator) GetAllowedBasePaths() []string {
|
func (pv *PathValidator) GetAllowedBasePaths() []string {
|
||||||
return append([]string(nil), pv.allowedBasePaths...)
|
return append([]string(nil), pv.allowedBasePaths...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,102 +11,104 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StateChange int
|
type StateChange int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PlayerCount StateChange = iota
|
PlayerCount StateChange = iota
|
||||||
Session
|
Session
|
||||||
)
|
)
|
||||||
|
|
||||||
var StateChanges = map[StateChange]string {
|
var StateChanges = map[StateChange]string{
|
||||||
PlayerCount: "player-count",
|
PlayerCount: "player-count",
|
||||||
Session: "session",
|
Session: "session",
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccServerInstance struct {
|
type AccServerInstance struct {
|
||||||
Model *model.Server
|
Model *model.Server
|
||||||
State *model.ServerState
|
State *model.ServerState
|
||||||
OnStateChange func(*model.ServerState, ...StateChange)
|
OnStateChange func(*model.ServerState, ...StateChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccServerInstance(server *model.Server, onStateChange func(*model.ServerState, ...StateChange)) *AccServerInstance {
|
func NewAccServerInstance(server *model.Server, onStateChange func(*model.ServerState, ...StateChange)) *AccServerInstance {
|
||||||
return &AccServerInstance{
|
return &AccServerInstance{
|
||||||
Model: server,
|
Model: server,
|
||||||
State: &model.ServerState{PlayerCount: 0},
|
State: &model.ServerState{PlayerCount: 0},
|
||||||
OnStateChange: onStateChange,
|
OnStateChange: onStateChange,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateRegexHandler struct {
|
type StateRegexHandler struct {
|
||||||
*regex_handler.RegexHandler
|
*regex_handler.RegexHandler
|
||||||
test string
|
test string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegexHandler(str string, test string) *StateRegexHandler {
|
func NewRegexHandler(str string, test string) *StateRegexHandler {
|
||||||
return &StateRegexHandler{
|
return &StateRegexHandler{
|
||||||
RegexHandler: regex_handler.New(str),
|
RegexHandler: regex_handler.New(str),
|
||||||
test: test,
|
test: test,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rh *StateRegexHandler) Test(line string) bool{
|
func (rh *StateRegexHandler) Test(line string) bool {
|
||||||
return strings.Contains(line, rh.test)
|
return strings.Contains(line, rh.test)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rh *StateRegexHandler) Count(line string) int{
|
func (rh *StateRegexHandler) Count(line string) int {
|
||||||
var count int = 0
|
var count int = 0
|
||||||
rh.Contains(line, func (strs ...string) {
|
rh.Contains(line, func(strs ...string) {
|
||||||
if len(strs) == 2 {
|
if len(strs) == 2 {
|
||||||
if ct, err := strconv.Atoi(strs[1]); err == nil {
|
if ct, err := strconv.Atoi(strs[1]); err == nil {
|
||||||
count = ct
|
count = ct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rh *StateRegexHandler) Change(line string) (string, string){
|
func (rh *StateRegexHandler) Change(line string) (string, string) {
|
||||||
var old string = ""
|
var old string = ""
|
||||||
var new string = ""
|
var new string = ""
|
||||||
rh.Contains(line, func (strs ...string) {
|
rh.Contains(line, func(strs ...string) {
|
||||||
if len(strs) == 3 {
|
if len(strs) == 3 {
|
||||||
old = strs[1]
|
old = strs[1]
|
||||||
new = strs[2]
|
new = strs[2]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return old, new
|
return old, new
|
||||||
}
|
}
|
||||||
|
|
||||||
func TailLogFile(path string, callback func(string)) {
|
func TailLogFile(path string, callback func(string)) {
|
||||||
file, _ := os.Open(path)
|
file, _ := os.Open(path)
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
file.Seek(0, os.SEEK_END) // Start at end of file
|
file.Seek(0, os.SEEK_END) // Start at end of file
|
||||||
reader := bufio.NewReader(file)
|
reader := bufio.NewReader(file)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
line, err := reader.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err == nil {
|
if err == nil {
|
||||||
callback(line)
|
callback(line)
|
||||||
} else {
|
} else {
|
||||||
time.Sleep(500 * time.Millisecond) // wait for new data
|
time.Sleep(500 * time.Millisecond) // wait for new data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogStateType int
|
type LogStateType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SessionChange LogStateType = iota
|
SessionChange LogStateType = iota
|
||||||
LeaderboardUpdate
|
LeaderboardUpdate
|
||||||
UDPCount
|
UDPCount
|
||||||
ClientsOnline
|
ClientsOnline
|
||||||
RemovingDeadConnection
|
RemovingDeadConnection
|
||||||
)
|
)
|
||||||
|
|
||||||
var logStateContain = map[LogStateType]string {
|
var logStateContain = map[LogStateType]string{
|
||||||
SessionChange: "Session changed",
|
SessionChange: "Session changed",
|
||||||
LeaderboardUpdate: "Updated leaderboard for",
|
LeaderboardUpdate: "Updated leaderboard for",
|
||||||
UDPCount: "Udp message count",
|
UDPCount: "Udp message count",
|
||||||
ClientsOnline: "client(s) online",
|
ClientsOnline: "client(s) online",
|
||||||
RemovingDeadConnection: "Removing dead connection",
|
RemovingDeadConnection: "Removing dead connection",
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionChangeRegex = NewRegexHandler(`Session changed: (\w+) -> (\w+)`, logStateContain[SessionChange])
|
var sessionChangeRegex = NewRegexHandler(`Session changed: (\w+) -> (\w+)`, logStateContain[SessionChange])
|
||||||
@@ -115,75 +117,77 @@ var udpCountRegex = NewRegexHandler(`Udp message count (\d+) client`, logStateCo
|
|||||||
var clientsOnlineRegex = NewRegexHandler(`(\d+) client\(s\) online`, logStateContain[ClientsOnline])
|
var clientsOnlineRegex = NewRegexHandler(`(\d+) client\(s\) online`, logStateContain[ClientsOnline])
|
||||||
var removingDeadConnectionsRegex = NewRegexHandler(`Removing dead connection`, logStateContain[RemovingDeadConnection])
|
var removingDeadConnectionsRegex = NewRegexHandler(`Removing dead connection`, logStateContain[RemovingDeadConnection])
|
||||||
|
|
||||||
var logStateRegex = map[LogStateType]*StateRegexHandler {
|
var logStateRegex = map[LogStateType]*StateRegexHandler{
|
||||||
SessionChange: sessionChangeRegex,
|
SessionChange: sessionChangeRegex,
|
||||||
LeaderboardUpdate: leaderboardUpdateRegex,
|
LeaderboardUpdate: leaderboardUpdateRegex,
|
||||||
UDPCount: udpCountRegex,
|
UDPCount: udpCountRegex,
|
||||||
ClientsOnline: clientsOnlineRegex,
|
ClientsOnline: clientsOnlineRegex,
|
||||||
RemovingDeadConnection: removingDeadConnectionsRegex,
|
RemovingDeadConnection: removingDeadConnectionsRegex,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *AccServerInstance) HandleLogLine(line string) {
|
func (instance *AccServerInstance) HandleLogLine(line string) {
|
||||||
for logState, regexHandler := range logStateRegex {
|
for logState, regexHandler := range logStateRegex {
|
||||||
if (regexHandler.Test(line)) {
|
if regexHandler.Test(line) {
|
||||||
switch logState {
|
switch logState {
|
||||||
case LeaderboardUpdate:
|
case LeaderboardUpdate:
|
||||||
case UDPCount:
|
case UDPCount:
|
||||||
case ClientsOnline:
|
case ClientsOnline:
|
||||||
count := regexHandler.Count(line)
|
count := regexHandler.Count(line)
|
||||||
instance.UpdatePlayerCount(count)
|
instance.UpdatePlayerCount(count)
|
||||||
case SessionChange:
|
case SessionChange:
|
||||||
_, new := regexHandler.Change(line)
|
_, new := regexHandler.Change(line)
|
||||||
instance.UpdateSessionChange(new)
|
|
||||||
case RemovingDeadConnection:
|
trackSession := model.ToTrackSession(new)
|
||||||
instance.UpdatePlayerCount(instance.State.PlayerCount - 1)
|
instance.UpdateSessionChange(trackSession)
|
||||||
}
|
case RemovingDeadConnection:
|
||||||
}
|
instance.UpdatePlayerCount(instance.State.PlayerCount - 1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *AccServerInstance) UpdateState(callback func(state *model.ServerState, changes *[]StateChange)) {
|
func (instance *AccServerInstance) UpdateState(callback func(state *model.ServerState, changes *[]StateChange)) {
|
||||||
state := instance.State
|
state := instance.State
|
||||||
changes := []StateChange{}
|
changes := []StateChange{}
|
||||||
state.Lock()
|
state.Lock()
|
||||||
defer state.Unlock()
|
defer state.Unlock()
|
||||||
callback(state, &changes)
|
callback(state, &changes)
|
||||||
if (len(changes) > 0) {
|
if len(changes) > 0 {
|
||||||
instance.OnStateChange(state, changes...)
|
instance.OnStateChange(state, changes...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *AccServerInstance) UpdatePlayerCount(count int) {
|
func (instance *AccServerInstance) UpdatePlayerCount(count int) {
|
||||||
if (count < 0) {
|
if count < 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
instance.UpdateState(func (state *model.ServerState, changes *[]StateChange) {
|
instance.UpdateState(func(state *model.ServerState, changes *[]StateChange) {
|
||||||
if (count == state.PlayerCount) {
|
if count == state.PlayerCount {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (count > 0 && state.PlayerCount == 0) {
|
if count > 0 && state.PlayerCount == 0 {
|
||||||
state.SessionStart = time.Now()
|
state.SessionStart = time.Now()
|
||||||
*changes = append(*changes, Session)
|
*changes = append(*changes, Session)
|
||||||
} else if (count == 0) {
|
} else if count == 0 {
|
||||||
state.SessionStart = time.Time{}
|
state.SessionStart = time.Time{}
|
||||||
*changes = append(*changes, Session)
|
*changes = append(*changes, Session)
|
||||||
}
|
}
|
||||||
state.PlayerCount = count
|
state.PlayerCount = count
|
||||||
*changes = append(*changes, PlayerCount)
|
*changes = append(*changes, PlayerCount)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *AccServerInstance) UpdateSessionChange(session string) {
|
func (instance *AccServerInstance) UpdateSessionChange(session model.TrackSession) {
|
||||||
instance.UpdateState(func (state *model.ServerState, changes *[]StateChange) {
|
instance.UpdateState(func(state *model.ServerState, changes *[]StateChange) {
|
||||||
if (session == state.Session) {
|
if session == state.Session {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (state.PlayerCount > 0) {
|
if state.PlayerCount > 0 {
|
||||||
state.SessionStart = time.Now()
|
state.SessionStart = time.Now()
|
||||||
} else {
|
} else {
|
||||||
state.SessionStart = time.Time{}
|
state.SessionStart = time.Time{}
|
||||||
}
|
}
|
||||||
state.Session = session
|
state.Session = session
|
||||||
*changes = append(*changes, Session)
|
*changes = append(*changes, Session)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func GenerateTestToken() (string, error) {
|
|||||||
jwtHandler := jwt.NewJWTHandler(testSecret)
|
jwtHandler := jwt.NewJWTHandler(testSecret)
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
token, err := jwtHandler.GenerateToken(user)
|
token, err := jwtHandler.GenerateToken(user.ID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to generate test token: %w", err)
|
return "", fmt.Errorf("failed to generate test token: %w", err)
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func GenerateTestTokenWithExpiry(expiryTime time.Time) (string, error) {
|
|||||||
testSecret = "test-secret-that-is-at-least-32-bytes-long-for-security"
|
testSecret = "test-secret-that-is-at-least-32-bytes-long-for-security"
|
||||||
}
|
}
|
||||||
jwtHandler := jwt.NewJWTHandler(testSecret)
|
jwtHandler := jwt.NewJWTHandler(testSecret)
|
||||||
|
|
||||||
// Create test user
|
// Create test user
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ func (m *MockStateHistoryRepository) GetSessionTypes(ctx context.Context, filter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group by session type
|
// Group by session type
|
||||||
sessionMap := make(map[string]map[string]bool) // session -> sessionID -> bool
|
sessionMap := make(map[model.TrackSession]map[string]bool) // session -> sessionID -> bool
|
||||||
for _, entry := range filteredEntries {
|
for _, entry := range filteredEntries {
|
||||||
if sessionMap[entry.Session] == nil {
|
if sessionMap[entry.Session] == nil {
|
||||||
sessionMap[entry.Session] = make(map[string]bool)
|
sessionMap[entry.Session] = make(map[string]bool)
|
||||||
@@ -360,7 +360,7 @@ func (m *MockStateHistoryRepository) GetRecentSessions(ctx context.Context, filt
|
|||||||
if maxPlayers > 0 {
|
if maxPlayers > 0 {
|
||||||
duration := int(maxDate.Sub(minDate).Minutes())
|
duration := int(maxDate.Sub(minDate).Minutes())
|
||||||
recentSessions = append(recentSessions, model.RecentSession{
|
recentSessions = append(recentSessions, model.RecentSession{
|
||||||
ID: uint(count + 1),
|
ID: entries[0].SessionID,
|
||||||
Date: minDate.Format("2006-01-02 15:04:05"),
|
Date: minDate.Format("2006-01-02 15:04:05"),
|
||||||
Type: entries[0].Session,
|
Type: entries[0].Session,
|
||||||
Track: entries[0].Track,
|
Track: entries[0].Track,
|
||||||
|
|||||||
18
tests/testdata/state_history_data.go
vendored
18
tests/testdata/state_history_data.go
vendored
@@ -22,7 +22,7 @@ func NewStateHistoryTestData(serverID uuid.UUID) *StateHistoryTestData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateStateHistory creates a basic state history entry
|
// CreateStateHistory creates a basic state history entry
|
||||||
func (td *StateHistoryTestData) CreateStateHistory(session string, track string, playerCount int, sessionID uuid.UUID) model.StateHistory {
|
func (td *StateHistoryTestData) CreateStateHistory(session model.TrackSession, track string, playerCount int, sessionID uuid.UUID) model.StateHistory {
|
||||||
return model.StateHistory{
|
return model.StateHistory{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
ServerID: td.ServerID,
|
ServerID: td.ServerID,
|
||||||
@@ -37,7 +37,7 @@ func (td *StateHistoryTestData) CreateStateHistory(session string, track string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateMultipleEntries creates multiple state history entries for the same session
|
// CreateMultipleEntries creates multiple state history entries for the same session
|
||||||
func (td *StateHistoryTestData) CreateMultipleEntries(session string, track string, playerCounts []int) []model.StateHistory {
|
func (td *StateHistoryTestData) CreateMultipleEntries(session model.TrackSession, track string, playerCounts []int) []model.StateHistory {
|
||||||
sessionID := uuid.New()
|
sessionID := uuid.New()
|
||||||
var entries []model.StateHistory
|
var entries []model.StateHistory
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ func CreateBasicFilter(serverID string) *model.StateHistoryFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateFilterWithSession creates a filter with session type
|
// CreateFilterWithSession creates a filter with session type
|
||||||
func CreateFilterWithSession(serverID string, session string) *model.StateHistoryFilter {
|
func CreateFilterWithSession(serverID string, session model.TrackSession) *model.StateHistoryFilter {
|
||||||
return &model.StateHistoryFilter{
|
return &model.StateHistoryFilter{
|
||||||
ServerBasedFilter: model.ServerBasedFilter{
|
ServerBasedFilter: model.ServerBasedFilter{
|
||||||
ServerID: serverID,
|
ServerID: serverID,
|
||||||
@@ -97,13 +97,13 @@ var SampleLogLines = []string{
|
|||||||
|
|
||||||
// ExpectedSessionChanges represents the expected session changes from parsing the sample log lines
|
// ExpectedSessionChanges represents the expected session changes from parsing the sample log lines
|
||||||
var ExpectedSessionChanges = []struct {
|
var ExpectedSessionChanges = []struct {
|
||||||
From string
|
From model.TrackSession
|
||||||
To string
|
To model.TrackSession
|
||||||
}{
|
}{
|
||||||
{"NONE", "PRACTICE"},
|
{model.SessionUnknown, model.SessionPractice},
|
||||||
{"PRACTICE", "QUALIFY"},
|
{model.SessionPractice, model.SessionQualify},
|
||||||
{"QUALIFY", "RACE"},
|
{model.SessionQualify, model.SessionRace},
|
||||||
{"RACE", "NONE"},
|
{model.SessionRace, model.SessionUnknown},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExpectedPlayerCounts represents the expected player counts from parsing the sample log lines
|
// ExpectedPlayerCounts represents the expected player counts from parsing the sample log lines
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func TestStateHistoryController_GetAll_Success(t *testing.T) {
|
|||||||
|
|
||||||
// Insert test data
|
// Insert test data
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
err := repo.Insert(helper.CreateContext(), &history)
|
err := repo.Insert(helper.CreateContext(), &history)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ func TestStateHistoryController_GetAll_Success(t *testing.T) {
|
|||||||
err = json.Unmarshal(body, &result)
|
err = json.Unmarshal(body, &result)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
tests.AssertEqual(t, 1, len(result))
|
tests.AssertEqual(t, 1, len(result))
|
||||||
tests.AssertEqual(t, "Practice", result[0].Session)
|
tests.AssertEqual(t, model.SessionPractice, result[0].Session)
|
||||||
tests.AssertEqual(t, 5, result[0].PlayerCount)
|
tests.AssertEqual(t, 5, result[0].PlayerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +107,8 @@ func TestStateHistoryController_GetAll_WithSessionFilter(t *testing.T) {
|
|||||||
// Insert test data with different sessions
|
// Insert test data with different sessions
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
|
|
||||||
practiceHistory := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
practiceHistory := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
raceHistory := testData.CreateStateHistory("Race", "spa", 10, uuid.New())
|
raceHistory := testData.CreateStateHistory(model.SessionRace, "spa", 10, uuid.New())
|
||||||
|
|
||||||
err := repo.Insert(helper.CreateContext(), &practiceHistory)
|
err := repo.Insert(helper.CreateContext(), &practiceHistory)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
@@ -124,7 +124,7 @@ func TestStateHistoryController_GetAll_WithSessionFilter(t *testing.T) {
|
|||||||
controller.NewStateHistoryController(stateHistoryService, routeGroups, GetTestAuthMiddleware(membershipService, inMemCache))
|
controller.NewStateHistoryController(stateHistoryService, routeGroups, GetTestAuthMiddleware(membershipService, inMemCache))
|
||||||
|
|
||||||
// Create request with session filter and authentication
|
// Create request with session filter and authentication
|
||||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/state-history?id=%s&session=Race", helper.TestData.ServerID.String()), nil)
|
req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/state-history?id=%s&session=R", helper.TestData.ServerID.String()), nil)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+tests.MustGenerateTestToken())
|
req.Header.Set("Authorization", "Bearer "+tests.MustGenerateTestToken())
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ func TestStateHistoryController_GetAll_WithSessionFilter(t *testing.T) {
|
|||||||
err = json.Unmarshal(body, &result)
|
err = json.Unmarshal(body, &result)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
tests.AssertEqual(t, 1, len(result))
|
tests.AssertEqual(t, 1, len(result))
|
||||||
tests.AssertEqual(t, "Race", result[0].Session)
|
tests.AssertEqual(t, model.SessionRace, result[0].Session)
|
||||||
tests.AssertEqual(t, 10, result[0].PlayerCount)
|
tests.AssertEqual(t, 10, result[0].PlayerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ func TestStateHistoryController_GetStatistics_Success(t *testing.T) {
|
|||||||
|
|
||||||
// Create entries with varying player counts
|
// Create entries with varying player counts
|
||||||
playerCounts := []int{5, 10, 15, 20, 25}
|
playerCounts := []int{5, 10, 15, 20, 25}
|
||||||
entries := testData.CreateMultipleEntries("Race", "spa", playerCounts)
|
entries := testData.CreateMultipleEntries(model.SessionRace, "spa", playerCounts)
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
err := repo.Insert(helper.CreateContext(), &entry)
|
err := repo.Insert(helper.CreateContext(), &entry)
|
||||||
@@ -475,7 +475,7 @@ func TestStateHistoryController_ContentType(t *testing.T) {
|
|||||||
|
|
||||||
// Insert test data
|
// Insert test data
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
err := repo.Insert(helper.CreateContext(), &history)
|
err := repo.Insert(helper.CreateContext(), &history)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
|
|
||||||
@@ -551,7 +551,7 @@ func TestStateHistoryController_ResponseStructure(t *testing.T) {
|
|||||||
|
|
||||||
// Insert test data
|
// Insert test data
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
err := repo.Insert(helper.CreateContext(), &history)
|
err := repo.Insert(helper.CreateContext(), &history)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func TestStateHistoryRepository_Insert_Success(t *testing.T) {
|
|||||||
|
|
||||||
// Create test data
|
// Create test data
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
|
|
||||||
// Test Insert
|
// Test Insert
|
||||||
err := repo.Insert(ctx, &history)
|
err := repo.Insert(ctx, &history)
|
||||||
@@ -65,7 +65,7 @@ func TestStateHistoryRepository_GetAll_Success(t *testing.T) {
|
|||||||
|
|
||||||
// Insert multiple entries
|
// Insert multiple entries
|
||||||
playerCounts := []int{0, 5, 10, 15, 10, 5, 0}
|
playerCounts := []int{0, 5, 10, 15, 10, 5, 0}
|
||||||
entries := testData.CreateMultipleEntries("Practice", "spa", playerCounts)
|
entries := testData.CreateMultipleEntries(model.SessionPractice, "spa", playerCounts)
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
err := repo.Insert(ctx, &entry)
|
err := repo.Insert(ctx, &entry)
|
||||||
@@ -101,8 +101,8 @@ func TestStateHistoryRepository_GetAll_WithFilter(t *testing.T) {
|
|||||||
// Create test data with different sessions
|
// Create test data with different sessions
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
|
|
||||||
practiceHistory := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
practiceHistory := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
raceHistory := testData.CreateStateHistory("Race", "spa", 15, uuid.New())
|
raceHistory := testData.CreateStateHistory(model.SessionRace, "spa", 15, uuid.New())
|
||||||
|
|
||||||
// Insert both
|
// Insert both
|
||||||
err := repo.Insert(ctx, &practiceHistory)
|
err := repo.Insert(ctx, &practiceHistory)
|
||||||
@@ -111,13 +111,13 @@ func TestStateHistoryRepository_GetAll_WithFilter(t *testing.T) {
|
|||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
|
|
||||||
// Test GetAll with session filter
|
// Test GetAll with session filter
|
||||||
filter := testdata.CreateFilterWithSession(helper.TestData.ServerID.String(), "Race")
|
filter := testdata.CreateFilterWithSession(helper.TestData.ServerID.String(), model.SessionRace)
|
||||||
result, err := repo.GetAll(ctx, filter)
|
result, err := repo.GetAll(ctx, filter)
|
||||||
|
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
tests.AssertNotNil(t, result)
|
tests.AssertNotNil(t, result)
|
||||||
tests.AssertEqual(t, 1, len(*result))
|
tests.AssertEqual(t, 1, len(*result))
|
||||||
tests.AssertEqual(t, "Race", (*result)[0].Session)
|
tests.AssertEqual(t, model.SessionRace, (*result)[0].Session)
|
||||||
tests.AssertEqual(t, 15, (*result)[0].PlayerCount)
|
tests.AssertEqual(t, 15, (*result)[0].PlayerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ func TestStateHistoryRepository_GetLastSessionID_Success(t *testing.T) {
|
|||||||
sessionID1 := uuid.New()
|
sessionID1 := uuid.New()
|
||||||
sessionID2 := uuid.New()
|
sessionID2 := uuid.New()
|
||||||
|
|
||||||
history1 := testData.CreateStateHistory("Practice", "spa", 5, sessionID1)
|
history1 := testData.CreateStateHistory(model.SessionPractice, "spa", 5, sessionID1)
|
||||||
history2 := testData.CreateStateHistory("Race", "spa", 10, sessionID2)
|
history2 := testData.CreateStateHistory(model.SessionRace, "spa", 10, sessionID2)
|
||||||
|
|
||||||
// Insert with a small delay to ensure ordering
|
// Insert with a small delay to ensure ordering
|
||||||
err := repo.Insert(ctx, &history1)
|
err := repo.Insert(ctx, &history1)
|
||||||
@@ -217,7 +217,7 @@ func TestStateHistoryRepository_GetSummaryStats_Success(t *testing.T) {
|
|||||||
sessionID2 := uuid.New()
|
sessionID2 := uuid.New()
|
||||||
|
|
||||||
// Practice session: 5, 10, 15 players
|
// Practice session: 5, 10, 15 players
|
||||||
practiceEntries := testData.CreateMultipleEntries("Practice", "spa", []int{5, 10, 15})
|
practiceEntries := testData.CreateMultipleEntries(model.SessionPractice, "spa", []int{5, 10, 15})
|
||||||
for i := range practiceEntries {
|
for i := range practiceEntries {
|
||||||
practiceEntries[i].SessionID = sessionID1
|
practiceEntries[i].SessionID = sessionID1
|
||||||
err := repo.Insert(ctx, &practiceEntries[i])
|
err := repo.Insert(ctx, &practiceEntries[i])
|
||||||
@@ -225,7 +225,7 @@ func TestStateHistoryRepository_GetSummaryStats_Success(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Race session: 20, 25, 30 players
|
// Race session: 20, 25, 30 players
|
||||||
raceEntries := testData.CreateMultipleEntries("Race", "spa", []int{20, 25, 30})
|
raceEntries := testData.CreateMultipleEntries(model.SessionRace, "spa", []int{20, 25, 30})
|
||||||
for i := range raceEntries {
|
for i := range raceEntries {
|
||||||
raceEntries[i].SessionID = sessionID2
|
raceEntries[i].SessionID = sessionID2
|
||||||
err := repo.Insert(ctx, &raceEntries[i])
|
err := repo.Insert(ctx, &raceEntries[i])
|
||||||
@@ -305,7 +305,7 @@ func TestStateHistoryRepository_GetTotalPlaytime_Success(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
ServerID: helper.TestData.ServerID,
|
ServerID: helper.TestData.ServerID,
|
||||||
Session: "Practice",
|
Session: model.SessionPractice,
|
||||||
Track: "spa",
|
Track: "spa",
|
||||||
PlayerCount: 5,
|
PlayerCount: 5,
|
||||||
DateCreated: baseTime,
|
DateCreated: baseTime,
|
||||||
@@ -316,7 +316,7 @@ func TestStateHistoryRepository_GetTotalPlaytime_Success(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
ServerID: helper.TestData.ServerID,
|
ServerID: helper.TestData.ServerID,
|
||||||
Session: "Practice",
|
Session: model.SessionPractice,
|
||||||
Track: "spa",
|
Track: "spa",
|
||||||
PlayerCount: 10,
|
PlayerCount: 10,
|
||||||
DateCreated: baseTime.Add(30 * time.Minute),
|
DateCreated: baseTime.Add(30 * time.Minute),
|
||||||
@@ -327,7 +327,7 @@ func TestStateHistoryRepository_GetTotalPlaytime_Success(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
ServerID: helper.TestData.ServerID,
|
ServerID: helper.TestData.ServerID,
|
||||||
Session: "Practice",
|
Session: model.SessionPractice,
|
||||||
Track: "spa",
|
Track: "spa",
|
||||||
PlayerCount: 8,
|
PlayerCount: 8,
|
||||||
DateCreated: baseTime.Add(60 * time.Minute),
|
DateCreated: baseTime.Add(60 * time.Minute),
|
||||||
@@ -391,7 +391,7 @@ func TestStateHistoryRepository_ConcurrentOperations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create and insert initial entry to ensure table exists and is properly set up
|
// Create and insert initial entry to ensure table exists and is properly set up
|
||||||
initialHistory := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
initialHistory := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
err := repo.Insert(ctx, &initialHistory)
|
err := repo.Insert(ctx, &initialHistory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to insert initial record: %v", err)
|
t.Fatalf("Failed to insert initial record: %v", err)
|
||||||
@@ -404,7 +404,7 @@ func TestStateHistoryRepository_ConcurrentOperations(t *testing.T) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
done <- true
|
done <- true
|
||||||
}()
|
}()
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
err := repo.Insert(ctx, &history)
|
err := repo.Insert(ctx, &history)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("Insert error: %v", err)
|
t.Logf("Insert error: %v", err)
|
||||||
@@ -462,7 +462,7 @@ func TestStateHistoryRepository_FilterEdgeCases(t *testing.T) {
|
|||||||
|
|
||||||
// Insert a test record to ensure the table is properly set up
|
// Insert a test record to ensure the table is properly set up
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
err := repo.Insert(ctx, &history)
|
err := repo.Insert(ctx, &history)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func TestJWT_GenerateAndValidateToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test JWT generation
|
// Test JWT generation
|
||||||
token, err := jwtHandler.GenerateToken(user)
|
token, err := jwtHandler.GenerateToken(user.ID.String())
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
tests.AssertNotNil(t, token)
|
tests.AssertNotNil(t, token)
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func TestConfigService_GetEventConfig_ValidFile(t *testing.T) {
|
|||||||
// Verify sessions
|
// Verify sessions
|
||||||
tests.AssertEqual(t, 3, len(eventConfig.Sessions))
|
tests.AssertEqual(t, 3, len(eventConfig.Sessions))
|
||||||
if len(eventConfig.Sessions) > 0 {
|
if len(eventConfig.Sessions) > 0 {
|
||||||
tests.AssertEqual(t, "P", eventConfig.Sessions[0].SessionType)
|
tests.AssertEqual(t, model.SessionPractice, eventConfig.Sessions[0].SessionType)
|
||||||
tests.AssertEqual(t, model.IntString(10), eventConfig.Sessions[0].SessionDurationMinutes)
|
tests.AssertEqual(t, model.IntString(10), eventConfig.Sessions[0].SessionDurationMinutes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TestStateHistoryService_GetAll_Success(t *testing.T) {
|
|||||||
|
|
||||||
// Insert test data directly into DB
|
// Insert test data directly into DB
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
err := repo.Insert(helper.CreateContext(), &history)
|
err := repo.Insert(helper.CreateContext(), &history)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ func TestStateHistoryService_GetAll_Success(t *testing.T) {
|
|||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
tests.AssertNotNil(t, result)
|
tests.AssertNotNil(t, result)
|
||||||
tests.AssertEqual(t, 1, len(*result))
|
tests.AssertEqual(t, 1, len(*result))
|
||||||
tests.AssertEqual(t, "Practice", (*result)[0].Session)
|
tests.AssertEqual(t, model.SessionPractice, (*result)[0].Session)
|
||||||
tests.AssertEqual(t, 5, (*result)[0].PlayerCount)
|
tests.AssertEqual(t, 5, (*result)[0].PlayerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ func TestStateHistoryService_GetAll_WithFilter(t *testing.T) {
|
|||||||
|
|
||||||
// Insert test data with different sessions
|
// Insert test data with different sessions
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
practiceHistory := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
practiceHistory := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
raceHistory := testData.CreateStateHistory("Race", "spa", 10, uuid.New())
|
raceHistory := testData.CreateStateHistory(model.SessionRace, "spa", 10, uuid.New())
|
||||||
|
|
||||||
err := repo.Insert(helper.CreateContext(), &practiceHistory)
|
err := repo.Insert(helper.CreateContext(), &practiceHistory)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
@@ -85,13 +85,13 @@ func TestStateHistoryService_GetAll_WithFilter(t *testing.T) {
|
|||||||
defer helper.ReleaseFiberCtx(app, ctx)
|
defer helper.ReleaseFiberCtx(app, ctx)
|
||||||
|
|
||||||
// Test GetAll with session filter
|
// Test GetAll with session filter
|
||||||
filter := testdata.CreateFilterWithSession(helper.TestData.ServerID.String(), "Race")
|
filter := testdata.CreateFilterWithSession(helper.TestData.ServerID.String(), model.SessionRace)
|
||||||
result, err := stateHistoryService.GetAll(ctx, filter)
|
result, err := stateHistoryService.GetAll(ctx, filter)
|
||||||
|
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
tests.AssertNotNil(t, result)
|
tests.AssertNotNil(t, result)
|
||||||
tests.AssertEqual(t, 1, len(*result))
|
tests.AssertEqual(t, 1, len(*result))
|
||||||
tests.AssertEqual(t, "Race", (*result)[0].Session)
|
tests.AssertEqual(t, model.SessionRace, (*result)[0].Session)
|
||||||
tests.AssertEqual(t, 10, (*result)[0].PlayerCount)
|
tests.AssertEqual(t, 10, (*result)[0].PlayerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ func TestStateHistoryService_Insert_Success(t *testing.T) {
|
|||||||
|
|
||||||
// Create test data
|
// Create test data
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, uuid.New())
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, uuid.New())
|
||||||
|
|
||||||
// Create proper Fiber context
|
// Create proper Fiber context
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
@@ -180,7 +180,7 @@ func TestStateHistoryService_GetLastSessionID_Success(t *testing.T) {
|
|||||||
// Insert test data
|
// Insert test data
|
||||||
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
testData := testdata.NewStateHistoryTestData(helper.TestData.ServerID)
|
||||||
sessionID := uuid.New()
|
sessionID := uuid.New()
|
||||||
history := testData.CreateStateHistory("Practice", "spa", 5, sessionID)
|
history := testData.CreateStateHistory(model.SessionPractice, "spa", 5, sessionID)
|
||||||
err := repo.Insert(helper.CreateContext(), &history)
|
err := repo.Insert(helper.CreateContext(), &history)
|
||||||
tests.AssertNoError(t, err)
|
tests.AssertNoError(t, err)
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ func TestStateHistoryService_GetStatistics_Success(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
ServerID: helper.TestData.ServerID,
|
ServerID: helper.TestData.ServerID,
|
||||||
Session: "Practice",
|
Session: model.SessionPractice,
|
||||||
Track: "spa",
|
Track: "spa",
|
||||||
PlayerCount: 5,
|
PlayerCount: 5,
|
||||||
DateCreated: baseTime,
|
DateCreated: baseTime,
|
||||||
@@ -265,7 +265,7 @@ func TestStateHistoryService_GetStatistics_Success(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
ServerID: helper.TestData.ServerID,
|
ServerID: helper.TestData.ServerID,
|
||||||
Session: "Practice",
|
Session: model.SessionPractice,
|
||||||
Track: "spa",
|
Track: "spa",
|
||||||
PlayerCount: 10,
|
PlayerCount: 10,
|
||||||
DateCreated: baseTime.Add(5 * time.Minute),
|
DateCreated: baseTime.Add(5 * time.Minute),
|
||||||
@@ -276,7 +276,7 @@ func TestStateHistoryService_GetStatistics_Success(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
ServerID: helper.TestData.ServerID,
|
ServerID: helper.TestData.ServerID,
|
||||||
Session: "Race",
|
Session: model.SessionRace,
|
||||||
Track: "spa",
|
Track: "spa",
|
||||||
PlayerCount: 15,
|
PlayerCount: 15,
|
||||||
DateCreated: baseTime.Add(10 * time.Minute),
|
DateCreated: baseTime.Add(10 * time.Minute),
|
||||||
@@ -404,7 +404,7 @@ func TestStateHistoryService_LogParsingWorkflow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify session changes were parsed correctly
|
// Verify session changes were parsed correctly
|
||||||
expectedSessions := []string{"PRACTICE", "QUALIFY", "RACE", "NONE"}
|
expectedSessions := []model.TrackSession{model.SessionPractice, model.SessionQualify, model.SessionRace}
|
||||||
sessionIndex := 0
|
sessionIndex := 0
|
||||||
|
|
||||||
for _, state := range stateChanges {
|
for _, state := range stateChanges {
|
||||||
@@ -433,7 +433,7 @@ func TestStateHistoryService_SessionChangeTracking(t *testing.T) {
|
|||||||
Name: "Test Server",
|
Name: "Test Server",
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionChanges []string
|
var sessionChanges []model.TrackSession
|
||||||
onStateChange := func(state *model.ServerState, changes ...tracking.StateChange) {
|
onStateChange := func(state *model.ServerState, changes ...tracking.StateChange) {
|
||||||
for _, change := range changes {
|
for _, change := range changes {
|
||||||
if change == tracking.Session {
|
if change == tracking.Session {
|
||||||
@@ -448,7 +448,7 @@ func TestStateHistoryService_SessionChangeTracking(t *testing.T) {
|
|||||||
|
|
||||||
// We'll add one session change at a time and wait briefly to ensure they're processed in order
|
// We'll add one session change at a time and wait briefly to ensure they're processed in order
|
||||||
for _, expected := range testdata.ExpectedSessionChanges {
|
for _, expected := range testdata.ExpectedSessionChanges {
|
||||||
line := "[2024-01-15 14:30:25.123] Session changed: " + expected.From + " -> " + expected.To
|
line := string("[2024-01-15 14:30:25.123] Session changed: " + expected.From + " -> " + expected.To)
|
||||||
instance.HandleLogLine(line)
|
instance.HandleLogLine(line)
|
||||||
// Small pause to ensure log processing completes
|
// Small pause to ensure log processing completes
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|||||||
Reference in New Issue
Block a user