alter primary keys to uuids and adjust the membership system

This commit is contained in:
Fran Jurmanović
2025-06-30 22:50:52 +02:00
parent caba5bae70
commit c17e7742ee
53 changed files with 12641 additions and 805 deletions

View File

@@ -6,115 +6,137 @@ import (
"os"
"strconv"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type IntString int
type IntBool int
// Config tracks configuration modifications
type Config struct {
ID uint `json:"id" gorm:"primaryKey"`
ServerID uint `json:"serverId" gorm:"not null"`
type Config struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid"`
ConfigFile string `json:"configFile" gorm:"not null"` // e.g. "settings.json"
OldConfig string `json:"oldConfig" gorm:"type:text"`
NewConfig string `json:"newConfig" gorm:"type:text"`
ChangedAt time.Time `json:"changedAt" gorm:"default:CURRENT_TIMESTAMP"`
}
// BeforeCreate is a GORM hook that runs before creating new config entries
func (c *Config) BeforeCreate(tx *gorm.DB) error {
if c.ID == uuid.Nil {
c.ID = uuid.New()
}
if c.ChangedAt.IsZero() {
c.ChangedAt = time.Now().UTC()
}
return nil
}
type Configurations struct {
Configuration Configuration `json:"configuration"`
AssistRules AssistRules `json:"assistRules"`
Event EventConfig `json:"event"`
EventRules EventRules `json:"eventRules"`
Configuration Configuration `json:"configuration"`
AssistRules AssistRules `json:"assistRules"`
Event EventConfig `json:"event"`
EventRules EventRules `json:"eventRules"`
Settings ServerSettings `json:"settings"`
}
type ServerSettings struct {
ServerName string `json:"serverName"`
AdminPassword string `json:"adminPassword"`
CarGroup string `json:"carGroup"`
TrackMedalsRequirement IntString `json:"trackMedalsRequirement"`
SafetyRatingRequirement IntString `json:"safetyRatingRequirement"`
RacecraftRatingRequirement IntString `json:"racecraftRatingRequirement"`
Password string `json:"password"`
SpectatorPassword string `json:"spectatorPassword"`
MaxCarSlots IntString `json:"maxCarSlots"`
DumpLeaderboards IntString `json:"dumpLeaderboards"`
IsRaceLocked IntString `json:"isRaceLocked"`
RandomizeTrackWhenEmpty IntString `json:"randomizeTrackWhenEmpty"`
CentralEntryListPath string `json:"centralEntryListPath"`
AllowAutoDQ IntString `json:"allowAutoDQ"`
ShortFormationLap IntString `json:"shortFormationLap"`
FormationLapType IntString `json:"formationLapType"`
IgnorePrematureDisconnects IntString `json:"ignorePrematureDisconnects"`
ServerName string `json:"serverName"`
AdminPassword string `json:"adminPassword"`
CarGroup string `json:"carGroup"`
TrackMedalsRequirement IntString `json:"trackMedalsRequirement"`
SafetyRatingRequirement IntString `json:"safetyRatingRequirement"`
RacecraftRatingRequirement IntString `json:"racecraftRatingRequirement"`
Password string `json:"password"`
SpectatorPassword string `json:"spectatorPassword"`
MaxCarSlots IntString `json:"maxCarSlots"`
DumpLeaderboards IntString `json:"dumpLeaderboards"`
IsRaceLocked IntString `json:"isRaceLocked"`
RandomizeTrackWhenEmpty IntString `json:"randomizeTrackWhenEmpty"`
CentralEntryListPath string `json:"centralEntryListPath"`
AllowAutoDQ IntString `json:"allowAutoDQ"`
ShortFormationLap IntString `json:"shortFormationLap"`
FormationLapType IntString `json:"formationLapType"`
IgnorePrematureDisconnects IntString `json:"ignorePrematureDisconnects"`
}
type EventConfig struct {
Track string `json:"track"`
PreRaceWaitingTimeSeconds IntString `json:"preRaceWaitingTimeSeconds"`
SessionOverTimeSeconds IntString `json:"sessionOverTimeSeconds"`
AmbientTemp IntString `json:"ambientTemp"`
CloudLevel float64 `json:"cloudLevel"`
Rain float64 `json:"rain"`
WeatherRandomness IntString `json:"weatherRandomness"`
PostQualySeconds IntString `json:"postQualySeconds"`
PostRaceSeconds IntString `json:"postRaceSeconds"`
SimracerWeatherConditions IntString `json:"simracerWeatherConditions"`
IsFixedConditionQualification IntString `json:"isFixedConditionQualification"`
Track string `json:"track"`
PreRaceWaitingTimeSeconds IntString `json:"preRaceWaitingTimeSeconds"`
SessionOverTimeSeconds IntString `json:"sessionOverTimeSeconds"`
AmbientTemp IntString `json:"ambientTemp"`
CloudLevel float64 `json:"cloudLevel"`
Rain float64 `json:"rain"`
WeatherRandomness IntString `json:"weatherRandomness"`
PostQualySeconds IntString `json:"postQualySeconds"`
PostRaceSeconds IntString `json:"postRaceSeconds"`
SimracerWeatherConditions IntString `json:"simracerWeatherConditions"`
IsFixedConditionQualification IntString `json:"isFixedConditionQualification"`
Sessions []Session `json:"sessions"`
}
type Session struct {
HourOfDay IntString `json:"hourOfDay"`
DayOfWeekend IntString `json:"dayOfWeekend"`
TimeMultiplier IntString `json:"timeMultiplier"`
SessionType string `json:"sessionType"`
SessionDurationMinutes IntString `json:"sessionDurationMinutes"`
HourOfDay IntString `json:"hourOfDay"`
DayOfWeekend IntString `json:"dayOfWeekend"`
TimeMultiplier IntString `json:"timeMultiplier"`
SessionType string `json:"sessionType"`
SessionDurationMinutes IntString `json:"sessionDurationMinutes"`
}
type AssistRules struct {
StabilityControlLevelMax IntString `json:"stabilityControlLevelMax"`
DisableAutosteer IntString `json:"disableAutosteer"`
DisableAutoLights IntString `json:"disableAutoLights"`
DisableAutoWiper IntString `json:"disableAutoWiper"`
DisableAutoEngineStart IntString `json:"disableAutoEngineStart"`
DisableAutoPitLimiter IntString `json:"disableAutoPitLimiter"`
DisableAutoGear IntString `json:"disableAutoGear"`
DisableAutoClutch IntString `json:"disableAutoClutch"`
DisableIdealLine IntString `json:"disableIdealLine"`
StabilityControlLevelMax IntString `json:"stabilityControlLevelMax"`
DisableAutosteer IntString `json:"disableAutosteer"`
DisableAutoLights IntString `json:"disableAutoLights"`
DisableAutoWiper IntString `json:"disableAutoWiper"`
DisableAutoEngineStart IntString `json:"disableAutoEngineStart"`
DisableAutoPitLimiter IntString `json:"disableAutoPitLimiter"`
DisableAutoGear IntString `json:"disableAutoGear"`
DisableAutoClutch IntString `json:"disableAutoClutch"`
DisableIdealLine IntString `json:"disableIdealLine"`
}
type EventRules struct {
QualifyStandingType IntString `json:"qualifyStandingType"`
PitWindowLengthSec IntString `json:"pitWindowLengthSec"`
DriverStIntStringTimeSec IntString `json:"driverStIntStringTimeSec"`
MandatoryPitstopCount IntString `json:"mandatoryPitstopCount"`
MaxTotalDrivingTime IntString `json:"maxTotalDrivingTime"`
IsRefuellingAllowedInRace IntBool `json:"isRefuellingAllowedInRace"`
IsRefuellingTimeFixed IntBool `json:"isRefuellingTimeFixed"`
IsMandatoryPitstopRefuellingRequired IntBool `json:"isMandatoryPitstopRefuellingRequired"`
IsMandatoryPitstopTyreChangeRequired IntBool `json:"isMandatoryPitstopTyreChangeRequired"`
IsMandatoryPitstopSwapDriverRequired IntBool `json:"isMandatoryPitstopSwapDriverRequired"`
TyreSetCount IntString `json:"tyreSetCount"`
QualifyStandingType IntString `json:"qualifyStandingType"`
PitWindowLengthSec IntString `json:"pitWindowLengthSec"`
DriverStIntStringTimeSec IntString `json:"driverStIntStringTimeSec"`
MandatoryPitstopCount IntString `json:"mandatoryPitstopCount"`
MaxTotalDrivingTime IntString `json:"maxTotalDrivingTime"`
IsRefuellingAllowedInRace IntBool `json:"isRefuellingAllowedInRace"`
IsRefuellingTimeFixed IntBool `json:"isRefuellingTimeFixed"`
IsMandatoryPitstopRefuellingRequired IntBool `json:"isMandatoryPitstopRefuellingRequired"`
IsMandatoryPitstopTyreChangeRequired IntBool `json:"isMandatoryPitstopTyreChangeRequired"`
IsMandatoryPitstopSwapDriverRequired IntBool `json:"isMandatoryPitstopSwapDriverRequired"`
TyreSetCount IntString `json:"tyreSetCount"`
}
type Configuration struct {
UdpPort IntString `json:"udpPort"`
TcpPort IntString `json:"tcpPort"`
MaxConnections IntString `json:"maxConnections"`
LanDiscovery IntString `json:"lanDiscovery"`
RegisterToLobby IntString `json:"registerToLobby"`
ConfigVersion IntString `json:"configVersion"`
UdpPort IntString `json:"udpPort"`
TcpPort IntString `json:"tcpPort"`
MaxConnections IntString `json:"maxConnections"`
LanDiscovery IntString `json:"lanDiscovery"`
RegisterToLobby IntString `json:"registerToLobby"`
ConfigVersion IntString `json:"configVersion"`
}
type SystemConfig struct {
ID uint `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
DefaultValue string `json:"defaultValue"`
Description string `json:"description"`
DateModified string `json:"dateModified"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
Key string `json:"key"`
Value string `json:"value"`
DefaultValue string `json:"defaultValue"`
Description string `json:"description"`
DateModified string `json:"dateModified"`
}
// BeforeCreate is a GORM hook that runs before creating new system config entries
func (sc *SystemConfig) BeforeCreate(tx *gorm.DB) error {
if sc.ID == uuid.Nil {
sc.ID = uuid.New()
}
return nil
}
// Known configuration keys
@@ -125,7 +147,7 @@ const (
// Cache keys
const (
CacheKeySystemConfig = "system_config_%s" // Format with config key
CacheKeySystemConfig = "system_config_%s" // Format with config key
)
func (i *IntBool) UnmarshalJSON(b []byte) error {
@@ -159,7 +181,7 @@ func (i IntBool) ToBool() bool {
func (i *IntString) UnmarshalJSON(b []byte) error {
var str string
if err := json.Unmarshal(b, &str); err == nil {
if (str == "") {
if str == "" {
*i = IntString(0)
} else {
n, err := strconv.Atoi(str)
@@ -184,7 +206,7 @@ func (i IntString) ToString() string {
return strconv.Itoa(int(i))
}
func (i IntString) ToInt() (int) {
func (i IntString) ToInt() int {
return int(i)
}
@@ -203,7 +225,7 @@ func (c *SystemConfig) Validate() error {
// Use default value if value is empty
c.Value = c.DefaultValue
}
// Check if path exists
if _, err := os.Stat(c.Value); os.IsNotExist(err) {
return fmt.Errorf("path does not exist: %s", c.Value)
@@ -218,4 +240,4 @@ func (c *SystemConfig) GetEffectiveValue() string {
return c.Value
}
return c.DefaultValue
}
}

View File

@@ -2,6 +2,9 @@ package model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// BaseFilter contains common filter fields that can be embedded in other filters
@@ -20,14 +23,14 @@ type DateRangeFilter struct {
// ServerBasedFilter adds server ID filtering capability
type ServerBasedFilter struct {
ServerID int `param:"id"`
ServerID string `param:"id"`
}
// ConfigFilter defines filtering options for Config queries
type ConfigFilter struct {
BaseFilter
ServerBasedFilter
ConfigFile string `query:"config_file"`
ConfigFile string `query:"config_file"`
ChangedAt time.Time `query:"changed_at" time_format:"2006-01-02T15:04:05Z07:00"`
}
@@ -37,6 +40,14 @@ type ApiFilter struct {
Api string `query:"api"`
}
// MembershipFilter defines filtering options for User queries
type MembershipFilter struct {
BaseFilter
Username string `query:"username"`
RoleName string `query:"role_name"`
RoleID string `query:"role_id"`
}
// Pagination returns the offset and limit for database queries
func (f *BaseFilter) Pagination() (offset, limit int) {
if f.Page < 1 {
@@ -64,4 +75,30 @@ func (f *DateRangeFilter) IsDateRangeValid() bool {
return true // If either date is not set, consider it valid
}
return f.StartDate.Before(f.EndDate)
}
}
// ApplyFilter applies the membership filter to a GORM query
func (f *MembershipFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
if f.Username != "" {
query = query.Where("username LIKE ?", "%"+f.Username+"%")
}
if f.RoleName != "" {
query = query.Joins("JOIN roles ON users.role_id = roles.id").Where("roles.name = ?", f.RoleName)
}
if f.RoleID != "" {
if roleUUID, err := uuid.Parse(f.RoleID); err == nil {
query = query.Where("role_id = ?", roleUUID)
}
}
return query
}
// Pagination returns the offset and limit for database queries
func (f *MembershipFilter) Pagination() (offset, limit int) {
return f.BaseFilter.Pagination()
}
// GetSorting returns the sort field and direction for database queries
func (f *MembershipFilter) GetSorting() (field string, desc bool) {
return f.BaseFilter.GetSorting()
}

View File

@@ -7,60 +7,61 @@ import (
"sync"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
BaseServerPath = "servers"
BaseServerPath = "servers"
ServiceNamePrefix = "ACC-Server"
)
// Server represents an ACC server instance
type Server struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Status ServiceStatus `json:"status" gorm:"-"`
IP string `gorm:"not null" json:"-"`
Port int `gorm:"not null" json:"-"`
Path string `gorm:"not null" json:"path"` // e.g. "/acc/servers/server1/"
ServiceName string `gorm:"not null" json:"serviceName"` // Windows service name
State *ServerState `gorm:"-" json:"state"`
DateCreated time.Time `json:"dateCreated"`
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
Name string `gorm:"not null" json:"name"`
Status ServiceStatus `json:"status" gorm:"-"`
IP string `gorm:"not null" json:"-"`
Port int `gorm:"not null" json:"-"`
Path string `gorm:"not null" json:"path"` // e.g. "/acc/servers/server1/"
ServiceName string `gorm:"not null" json:"serviceName"` // Windows service name
State *ServerState `gorm:"-" json:"state"`
DateCreated time.Time `json:"dateCreated"`
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
}
type PlayerState struct {
CarID int // Car ID in broadcast packets
DriverName string // Optional: pulled from registration packet
TeamName string
CarModel string
CurrentLap int
LastLapTime int // in milliseconds
BestLapTime int // in milliseconds
Position int
ConnectedAt time.Time
DisconnectedAt *time.Time
IsConnected bool
CarID int // Car ID in broadcast packets
DriverName string // Optional: pulled from registration packet
TeamName string
CarModel string
CurrentLap int
LastLapTime int // in milliseconds
BestLapTime int // in milliseconds
Position int
ConnectedAt time.Time
DisconnectedAt *time.Time
IsConnected bool
}
type State struct {
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
// Players map[int]*PlayerState
// etc.
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
// Players map[int]*PlayerState
// etc.
}
type ServerState struct {
sync.RWMutex
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
Track string `json:"track"`
MaxConnections int `json:"maxConnections"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
// Players map[int]*PlayerState
// etc.
sync.RWMutex
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
Track string `json:"track"`
MaxConnections int `json:"maxConnections"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
// Players map[int]*PlayerState
// etc.
}
// ServerFilter defines filtering options for Server queries
@@ -75,8 +76,10 @@ type ServerFilter struct {
// ApplyFilter implements the Filterable interface
func (f *ServerFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
// Apply server filter
if f.ServerID != 0 {
query = query.Where("id = ?", f.ServerID)
if f.ServerID != "" {
if serverUUID, err := uuid.Parse(f.ServerID); err == nil {
query = query.Where("id = ?", serverUUID)
}
}
return query
@@ -88,6 +91,11 @@ func (s *Server) BeforeCreate(tx *gorm.DB) error {
return errors.New("server name is required")
}
// Generate UUID if not set
if s.ID == uuid.Nil {
s.ID = uuid.New()
}
// Generate service name and config path if not set
if s.ServiceName == "" {
s.ServiceName = s.GenerateServiceName()
@@ -107,8 +115,8 @@ func (s *Server) BeforeCreate(tx *gorm.DB) error {
// GenerateServiceName creates a unique service name based on the server name
func (s *Server) GenerateServiceName() string {
// If ID is set, use it
if s.ID > 0 {
return fmt.Sprintf("%s-%d", ServiceNamePrefix, s.ID)
if s.ID != uuid.Nil {
return fmt.Sprintf("%s-%s", ServiceNamePrefix, s.ID.String()[:8])
}
// Otherwise use a timestamp-based unique identifier
return fmt.Sprintf("%s-%d", ServiceNamePrefix, time.Now().UnixNano())
@@ -120,14 +128,14 @@ func (s *Server) GenerateServerPath(steamCMDPath string) string {
if s.ServiceName == "" {
s.ServiceName = s.GenerateServiceName()
}
if (steamCMDPath == "") {
if steamCMDPath == "" {
steamCMDPath = BaseServerPath
}
return filepath.Join(steamCMDPath, "servers", s.ServiceName)
}
func (s *Server) GetServerPath() string {
if (!s.FromSteamCMD) {
if !s.FromSteamCMD {
return s.Path
}
return filepath.Join(s.Path, "server")
@@ -138,7 +146,7 @@ func (s *Server) GetConfigPath() string {
}
func (s *Server) GetLogPath() string {
if (!s.FromSteamCMD) {
if !s.FromSteamCMD {
return s.Path
}
return filepath.Join(s.GetServerPath(), "log")
@@ -149,4 +157,4 @@ func (s *Server) Validate() error {
return errors.New("server name is required")
}
return nil
}
}

View File

@@ -3,6 +3,7 @@ package model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -10,18 +11,20 @@ import (
type StateHistoryFilter struct {
ServerBasedFilter // Adds server ID from path parameter
DateRangeFilter // Adds date range filtering
// Additional fields specific to state history
Session string `query:"session"`
MinPlayers *int `query:"min_players"`
MaxPlayers *int `query:"max_players"`
Session string `query:"session"`
MinPlayers *int `query:"min_players"`
MaxPlayers *int `query:"max_players"`
}
// ApplyFilter implements the Filterable interface
func (f *StateHistoryFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
// Apply server filter
if f.ServerID != 0 {
query = query.Where("server_id = ?", f.ServerID)
if f.ServerID != "" {
if serverUUID, err := uuid.Parse(f.ServerID); err == nil {
query = query.Where("server_id = ?", serverUUID)
}
}
// Apply date range filter if set
@@ -50,13 +53,27 @@ func (f *StateHistoryFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
}
type StateHistory struct {
ID uint `gorm:"primaryKey" json:"id"`
ServerID uint `json:"serverId" gorm:"not null"`
Session string `json:"session"`
Track string `json:"track"`
PlayerCount int `json:"playerCount"`
DateCreated time.Time `json:"dateCreated"`
SessionStart time.Time `json:"sessionStart"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
SessionID uint `json:"sessionId" gorm:"not null;default:0"` // Unique identifier for each session/event
}
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid"`
Session string `json:"session"`
Track string `json:"track"`
PlayerCount int `json:"playerCount"`
DateCreated time.Time `json:"dateCreated"`
SessionStart time.Time `json:"sessionStart"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
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
func (sh *StateHistory) BeforeCreate(tx *gorm.DB) error {
if sh.ID == uuid.Nil {
sh.ID = uuid.New()
}
if sh.SessionID == uuid.Nil {
sh.SessionID = uuid.New()
}
if sh.DateCreated.IsZero() {
sh.DateCreated = time.Now().UTC()
}
return nil
}

View File

@@ -12,12 +12,13 @@ import (
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// SteamCredentials represents stored Steam login credentials
type SteamCredentials struct {
ID uint `gorm:"primaryKey" json:"id"`
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
Username string `gorm:"not null" json:"username"`
Password string `gorm:"not null" json:"-"` // Encrypted, not exposed in JSON
DateCreated time.Time `json:"dateCreated"`
@@ -31,6 +32,10 @@ func (SteamCredentials) TableName() string {
// BeforeCreate is a GORM hook that runs before creating new credentials
func (s *SteamCredentials) BeforeCreate(tx *gorm.DB) error {
if s.ID == uuid.Nil {
s.ID = uuid.New()
}
now := time.Now().UTC()
if s.DateCreated.IsZero() {
s.DateCreated = now