add step list for server creation
This commit is contained in:
@@ -6,20 +6,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatusCache represents a cached server status with expiration
|
||||
type StatusCache struct {
|
||||
Status ServiceStatus
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// CacheConfig holds configuration for cache behavior
|
||||
type CacheConfig struct {
|
||||
ExpirationTime time.Duration // How long before a cache entry expires
|
||||
ThrottleTime time.Duration // Minimum time between status checks
|
||||
DefaultStatus ServiceStatus // Default status to return when throttled
|
||||
ExpirationTime time.Duration
|
||||
ThrottleTime time.Duration
|
||||
DefaultStatus ServiceStatus
|
||||
}
|
||||
|
||||
// ServerStatusCache manages cached server statuses
|
||||
type ServerStatusCache struct {
|
||||
sync.RWMutex
|
||||
cache map[string]*StatusCache
|
||||
@@ -27,7 +24,6 @@ type ServerStatusCache struct {
|
||||
lastChecked map[string]time.Time
|
||||
}
|
||||
|
||||
// NewServerStatusCache creates a new server status cache
|
||||
func NewServerStatusCache(config CacheConfig) *ServerStatusCache {
|
||||
return &ServerStatusCache{
|
||||
cache: make(map[string]*StatusCache),
|
||||
@@ -36,12 +32,10 @@ func NewServerStatusCache(config CacheConfig) *ServerStatusCache {
|
||||
}
|
||||
}
|
||||
|
||||
// GetStatus retrieves the cached status or indicates if a fresh check is needed
|
||||
func (c *ServerStatusCache) GetStatus(serviceName string) (ServiceStatus, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
// Check if we're being throttled
|
||||
if lastCheck, exists := c.lastChecked[serviceName]; exists {
|
||||
if time.Since(lastCheck) < c.config.ThrottleTime {
|
||||
if cached, ok := c.cache[serviceName]; ok {
|
||||
@@ -51,7 +45,6 @@ func (c *ServerStatusCache) GetStatus(serviceName string) (ServiceStatus, bool)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have a valid cached entry
|
||||
if cached, ok := c.cache[serviceName]; ok {
|
||||
if time.Since(cached.UpdatedAt) < c.config.ExpirationTime {
|
||||
return cached.Status, false
|
||||
@@ -61,7 +54,6 @@ func (c *ServerStatusCache) GetStatus(serviceName string) (ServiceStatus, bool)
|
||||
return StatusUnknown, true
|
||||
}
|
||||
|
||||
// UpdateStatus updates the cache with a new status
|
||||
func (c *ServerStatusCache) UpdateStatus(serviceName string, status ServiceStatus) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -73,7 +65,6 @@ func (c *ServerStatusCache) UpdateStatus(serviceName string, status ServiceStatu
|
||||
c.lastChecked[serviceName] = time.Now()
|
||||
}
|
||||
|
||||
// InvalidateStatus removes a specific service from the cache
|
||||
func (c *ServerStatusCache) InvalidateStatus(serviceName string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -82,7 +73,6 @@ func (c *ServerStatusCache) InvalidateStatus(serviceName string) {
|
||||
delete(c.lastChecked, serviceName)
|
||||
}
|
||||
|
||||
// Clear removes all entries from the cache
|
||||
func (c *ServerStatusCache) Clear() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -91,13 +81,11 @@ func (c *ServerStatusCache) Clear() {
|
||||
c.lastChecked = make(map[string]time.Time)
|
||||
}
|
||||
|
||||
// LookupCache provides a generic cache for lookup data
|
||||
type LookupCache struct {
|
||||
sync.RWMutex
|
||||
data map[string]interface{}
|
||||
}
|
||||
|
||||
// NewLookupCache creates a new lookup cache
|
||||
func NewLookupCache() *LookupCache {
|
||||
logging.Debug("Initializing new LookupCache")
|
||||
return &LookupCache{
|
||||
@@ -105,7 +93,6 @@ func NewLookupCache() *LookupCache {
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a cached value by key
|
||||
func (c *LookupCache) Get(key string) (interface{}, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
@@ -119,7 +106,6 @@ func (c *LookupCache) Get(key string) (interface{}, bool) {
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// Set stores a value in the cache
|
||||
func (c *LookupCache) Set(key string, value interface{}) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -128,7 +114,6 @@ func (c *LookupCache) Set(key string, value interface{}) {
|
||||
logging.Debug("Cache SET for key: %s", key)
|
||||
}
|
||||
|
||||
// Clear removes all entries from the cache
|
||||
func (c *LookupCache) Clear() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -137,13 +122,11 @@ func (c *LookupCache) Clear() {
|
||||
logging.Debug("Cache CLEARED")
|
||||
}
|
||||
|
||||
// ConfigEntry represents a cached configuration entry with its update time
|
||||
type ConfigEntry[T any] struct {
|
||||
Data T
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// getConfigFromCache is a generic helper function to retrieve cached configs
|
||||
func getConfigFromCache[T any](cache map[string]*ConfigEntry[T], serverID string, expirationTime time.Duration) (*T, bool) {
|
||||
if entry, ok := cache[serverID]; ok {
|
||||
if time.Since(entry.UpdatedAt) < expirationTime {
|
||||
@@ -157,7 +140,6 @@ func getConfigFromCache[T any](cache map[string]*ConfigEntry[T], serverID string
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// updateConfigInCache is a generic helper function to update cached configs
|
||||
func updateConfigInCache[T any](cache map[string]*ConfigEntry[T], serverID string, data T) {
|
||||
cache[serverID] = &ConfigEntry[T]{
|
||||
Data: data,
|
||||
@@ -166,7 +148,6 @@ func updateConfigInCache[T any](cache map[string]*ConfigEntry[T], serverID strin
|
||||
logging.Debug("Config cache SET for server ID: %s", serverID)
|
||||
}
|
||||
|
||||
// ServerConfigCache manages cached server configurations
|
||||
type ServerConfigCache struct {
|
||||
sync.RWMutex
|
||||
configuration map[string]*ConfigEntry[Configuration]
|
||||
@@ -177,7 +158,6 @@ type ServerConfigCache struct {
|
||||
config CacheConfig
|
||||
}
|
||||
|
||||
// NewServerConfigCache creates a new server configuration cache
|
||||
func NewServerConfigCache(config CacheConfig) *ServerConfigCache {
|
||||
logging.Debug("Initializing new ServerConfigCache with expiration time: %v, throttle time: %v", config.ExpirationTime, config.ThrottleTime)
|
||||
return &ServerConfigCache{
|
||||
@@ -190,7 +170,6 @@ func NewServerConfigCache(config CacheConfig) *ServerConfigCache {
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfiguration retrieves a cached configuration
|
||||
func (c *ServerConfigCache) GetConfiguration(serverID string) (*Configuration, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
@@ -198,7 +177,6 @@ func (c *ServerConfigCache) GetConfiguration(serverID string) (*Configuration, b
|
||||
return getConfigFromCache(c.configuration, serverID, c.config.ExpirationTime)
|
||||
}
|
||||
|
||||
// GetAssistRules retrieves cached assist rules
|
||||
func (c *ServerConfigCache) GetAssistRules(serverID string) (*AssistRules, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
@@ -206,7 +184,6 @@ func (c *ServerConfigCache) GetAssistRules(serverID string) (*AssistRules, bool)
|
||||
return getConfigFromCache(c.assistRules, serverID, c.config.ExpirationTime)
|
||||
}
|
||||
|
||||
// GetEvent retrieves cached event configuration
|
||||
func (c *ServerConfigCache) GetEvent(serverID string) (*EventConfig, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
@@ -214,7 +191,6 @@ func (c *ServerConfigCache) GetEvent(serverID string) (*EventConfig, bool) {
|
||||
return getConfigFromCache(c.event, serverID, c.config.ExpirationTime)
|
||||
}
|
||||
|
||||
// GetEventRules retrieves cached event rules
|
||||
func (c *ServerConfigCache) GetEventRules(serverID string) (*EventRules, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
@@ -222,7 +198,6 @@ func (c *ServerConfigCache) GetEventRules(serverID string) (*EventRules, bool) {
|
||||
return getConfigFromCache(c.eventRules, serverID, c.config.ExpirationTime)
|
||||
}
|
||||
|
||||
// GetSettings retrieves cached server settings
|
||||
func (c *ServerConfigCache) GetSettings(serverID string) (*ServerSettings, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
@@ -230,7 +205,6 @@ func (c *ServerConfigCache) GetSettings(serverID string) (*ServerSettings, bool)
|
||||
return getConfigFromCache(c.settings, serverID, c.config.ExpirationTime)
|
||||
}
|
||||
|
||||
// UpdateConfiguration updates the configuration cache
|
||||
func (c *ServerConfigCache) UpdateConfiguration(serverID string, config Configuration) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -238,7 +212,6 @@ func (c *ServerConfigCache) UpdateConfiguration(serverID string, config Configur
|
||||
updateConfigInCache(c.configuration, serverID, config)
|
||||
}
|
||||
|
||||
// UpdateAssistRules updates the assist rules cache
|
||||
func (c *ServerConfigCache) UpdateAssistRules(serverID string, rules AssistRules) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -246,7 +219,6 @@ func (c *ServerConfigCache) UpdateAssistRules(serverID string, rules AssistRules
|
||||
updateConfigInCache(c.assistRules, serverID, rules)
|
||||
}
|
||||
|
||||
// UpdateEvent updates the event configuration cache
|
||||
func (c *ServerConfigCache) UpdateEvent(serverID string, event EventConfig) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -254,7 +226,6 @@ func (c *ServerConfigCache) UpdateEvent(serverID string, event EventConfig) {
|
||||
updateConfigInCache(c.event, serverID, event)
|
||||
}
|
||||
|
||||
// UpdateEventRules updates the event rules cache
|
||||
func (c *ServerConfigCache) UpdateEventRules(serverID string, rules EventRules) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -262,7 +233,6 @@ func (c *ServerConfigCache) UpdateEventRules(serverID string, rules EventRules)
|
||||
updateConfigInCache(c.eventRules, serverID, rules)
|
||||
}
|
||||
|
||||
// UpdateSettings updates the server settings cache
|
||||
func (c *ServerConfigCache) UpdateSettings(serverID string, settings ServerSettings) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -270,7 +240,6 @@ func (c *ServerConfigCache) UpdateSettings(serverID string, settings ServerSetti
|
||||
updateConfigInCache(c.settings, serverID, settings)
|
||||
}
|
||||
|
||||
// InvalidateServerCache removes all cached configurations for a specific server
|
||||
func (c *ServerConfigCache) InvalidateServerCache(serverID string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -283,7 +252,6 @@ func (c *ServerConfigCache) InvalidateServerCache(serverID string) {
|
||||
delete(c.settings, serverID)
|
||||
}
|
||||
|
||||
// Clear removes all entries from the cache
|
||||
func (c *ServerConfigCache) Clear() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
@@ -13,17 +13,15 @@ import (
|
||||
type IntString int
|
||||
type IntBool int
|
||||
|
||||
// Config tracks configuration modifications
|
||||
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"
|
||||
ConfigFile string `json:"configFile" gorm:"not null"`
|
||||
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()
|
||||
@@ -121,8 +119,6 @@ type Configuration struct {
|
||||
ConfigVersion IntString `json:"configVersion"`
|
||||
}
|
||||
|
||||
// Known configuration keys
|
||||
|
||||
func (i *IntBool) UnmarshalJSON(b []byte) error {
|
||||
var str int
|
||||
if err := json.Unmarshal(b, &str); err == nil && str <= 1 {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BaseFilter contains common filter fields that can be embedded in other filters
|
||||
type BaseFilter struct {
|
||||
Page int `query:"page"`
|
||||
PageSize int `query:"page_size"`
|
||||
@@ -15,18 +14,15 @@ type BaseFilter struct {
|
||||
SortDesc bool `query:"sort_desc"`
|
||||
}
|
||||
|
||||
// DateRangeFilter adds date range filtering capabilities
|
||||
type DateRangeFilter struct {
|
||||
StartDate time.Time `query:"start_date" time_format:"2006-01-02T15:04:05Z07:00"`
|
||||
EndDate time.Time `query:"end_date" time_format:"2006-01-02T15:04:05Z07:00"`
|
||||
}
|
||||
|
||||
// ServerBasedFilter adds server ID filtering capability
|
||||
type ServerBasedFilter struct {
|
||||
ServerID string `param:"id"`
|
||||
}
|
||||
|
||||
// ConfigFilter defines filtering options for Config queries
|
||||
type ConfigFilter struct {
|
||||
BaseFilter
|
||||
ServerBasedFilter
|
||||
@@ -34,13 +30,11 @@ type ConfigFilter struct {
|
||||
ChangedAt time.Time `query:"changed_at" time_format:"2006-01-02T15:04:05Z07:00"`
|
||||
}
|
||||
|
||||
// ApiFilter defines filtering options for Api queries
|
||||
type ServiceControlFilter struct {
|
||||
BaseFilter
|
||||
ServiceControl string `query:"serviceControl"`
|
||||
}
|
||||
|
||||
// MembershipFilter defines filtering options for User queries
|
||||
type MembershipFilter struct {
|
||||
BaseFilter
|
||||
Username string `query:"username"`
|
||||
@@ -48,36 +42,32 @@ type MembershipFilter struct {
|
||||
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 {
|
||||
f.Page = 1
|
||||
}
|
||||
if f.PageSize < 1 {
|
||||
f.PageSize = 10 // Default page size
|
||||
f.PageSize = 10
|
||||
}
|
||||
offset = (f.Page - 1) * f.PageSize
|
||||
limit = f.PageSize
|
||||
return
|
||||
}
|
||||
|
||||
// GetSorting returns the sort field and direction for database queries
|
||||
func (f *BaseFilter) GetSorting() (field string, desc bool) {
|
||||
if f.SortBy == "" {
|
||||
return "id", false // Default sorting
|
||||
return "id", false
|
||||
}
|
||||
return f.SortBy, f.SortDesc
|
||||
}
|
||||
|
||||
// IsDateRangeValid checks if both dates are set and start date is before end date
|
||||
func (f *DateRangeFilter) IsDateRangeValid() bool {
|
||||
if f.StartDate.IsZero() || f.EndDate.IsZero() {
|
||||
return true // If either date is not set, consider it valid
|
||||
return true
|
||||
}
|
||||
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+"%")
|
||||
@@ -93,12 +83,10 @@ func (f *MembershipFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
package model
|
||||
|
||||
// Track represents a track and its capacity
|
||||
type Track struct {
|
||||
Name string `json:"track" gorm:"primaryKey;size:50"`
|
||||
UniquePitBoxes int `json:"unique_pit_boxes"`
|
||||
PrivateServerSlots int `json:"private_server_slots"`
|
||||
}
|
||||
|
||||
// CarModel represents a car model mapping
|
||||
type CarModel struct {
|
||||
Value int `json:"value" gorm:"primaryKey"`
|
||||
CarModel string `json:"car_model"`
|
||||
}
|
||||
|
||||
// DriverCategory represents driver skill categories
|
||||
type DriverCategory struct {
|
||||
Value int `json:"value" gorm:"primaryKey"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
// CupCategory represents championship cup categories
|
||||
type CupCategory struct {
|
||||
Value int `json:"value" gorm:"primaryKey"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
// SessionType represents session types
|
||||
type SessionType struct {
|
||||
Value int `json:"value" gorm:"primaryKey"`
|
||||
SessionType string `json:"session_type"`
|
||||
|
||||
@@ -32,8 +32,6 @@ type BaseModel struct {
|
||||
DateUpdated time.Time `json:"dateUpdated"`
|
||||
}
|
||||
|
||||
// Init
|
||||
// Initializes base model with DateCreated, DateUpdated, and Id values.
|
||||
func (cm *BaseModel) Init() {
|
||||
date := time.Now()
|
||||
cm.Id = uuid.NewString()
|
||||
|
||||
@@ -5,15 +5,13 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Permission represents an action that can be performed in the system.
|
||||
type Permission struct {
|
||||
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
|
||||
Name string `json:"name" gorm:"unique_index;not null"`
|
||||
}
|
||||
|
||||
// BeforeCreate is a GORM hook that runs before creating new credentials
|
||||
func (s *Permission) BeforeCreate(tx *gorm.DB) error {
|
||||
s.ID = uuid.New()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package model
|
||||
|
||||
// Permission constants
|
||||
const (
|
||||
ServerView = "server.view"
|
||||
ServerCreate = "server.create"
|
||||
@@ -27,7 +26,6 @@ const (
|
||||
MembershipEdit = "membership.edit"
|
||||
)
|
||||
|
||||
// AllPermissions returns a slice of all permission strings.
|
||||
func AllPermissions() []string {
|
||||
return []string{
|
||||
ServerView,
|
||||
|
||||
@@ -5,16 +5,14 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Role represents a user role in the system.
|
||||
type Role struct {
|
||||
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
|
||||
Name string `json:"name" gorm:"unique_index;not null"`
|
||||
Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
|
||||
}
|
||||
|
||||
// BeforeCreate is a GORM hook that runs before creating new credentials
|
||||
func (s *Role) BeforeCreate(tx *gorm.DB) error {
|
||||
s.ID = uuid.New()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ const (
|
||||
ServiceNamePrefix = "ACC-Server"
|
||||
)
|
||||
|
||||
// Server represents an ACC server instance
|
||||
type ServerAPI struct {
|
||||
Name string `json:"name"`
|
||||
Status ServiceStatus `json:"status"`
|
||||
@@ -35,28 +34,27 @@ func (s *Server) ToServerAPI() *ServerAPI {
|
||||
}
|
||||
}
|
||||
|
||||
// Server represents an ACC server instance
|
||||
type Server struct {
|
||||
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
|
||||
Path string `gorm:"not null" json:"path"`
|
||||
ServiceName string `gorm:"not null" json:"serviceName"`
|
||||
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
|
||||
CarID int
|
||||
DriverName string
|
||||
TeamName string
|
||||
CarModel string
|
||||
CurrentLap int
|
||||
LastLapTime int // in milliseconds
|
||||
BestLapTime int // in milliseconds
|
||||
LastLapTime int
|
||||
BestLapTime int
|
||||
Position int
|
||||
ConnectedAt time.Time
|
||||
DisconnectedAt *time.Time
|
||||
@@ -67,8 +65,6 @@ type State struct {
|
||||
Session string `json:"session"`
|
||||
SessionStart time.Time `json:"sessionStart"`
|
||||
PlayerCount int `json:"playerCount"`
|
||||
// Players map[int]*PlayerState
|
||||
// etc.
|
||||
}
|
||||
|
||||
type ServerState struct {
|
||||
@@ -79,11 +75,8 @@ type ServerState struct {
|
||||
Track string `json:"track"`
|
||||
MaxConnections int `json:"maxConnections"`
|
||||
SessionDurationMinutes int `json:"sessionDurationMinutes"`
|
||||
// Players map[int]*PlayerState
|
||||
// etc.
|
||||
}
|
||||
|
||||
// ServerFilter defines filtering options for Server queries
|
||||
type ServerFilter struct {
|
||||
BaseFilter
|
||||
ServerBasedFilter
|
||||
@@ -92,9 +85,7 @@ type ServerFilter struct {
|
||||
Status string `query:"status"`
|
||||
}
|
||||
|
||||
// ApplyFilter implements the Filterable interface
|
||||
func (f *ServerFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
|
||||
// Apply server filter
|
||||
if f.ServerID != "" {
|
||||
if serverUUID, err := uuid.Parse(f.ServerID); err == nil {
|
||||
query = query.Where("id = ?", serverUUID)
|
||||
@@ -110,16 +101,13 @@ func (s *Server) GenerateUUID() {
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeCreate is a GORM hook that runs before creating a new server
|
||||
func (s *Server) BeforeCreate(tx *gorm.DB) error {
|
||||
if s.Name == "" {
|
||||
return errors.New("server name is required")
|
||||
}
|
||||
|
||||
// Generate UUID if not set
|
||||
s.GenerateUUID()
|
||||
|
||||
// Generate service name and config path if not set
|
||||
if s.ServiceName == "" {
|
||||
s.ServiceName = s.GenerateServiceName()
|
||||
}
|
||||
@@ -127,7 +115,6 @@ func (s *Server) BeforeCreate(tx *gorm.DB) error {
|
||||
s.Path = s.GenerateServerPath(BaseServerPath)
|
||||
}
|
||||
|
||||
// Set creation date if not set
|
||||
if s.DateCreated.IsZero() {
|
||||
s.DateCreated = time.Now().UTC()
|
||||
}
|
||||
@@ -135,19 +122,14 @@ func (s *Server) BeforeCreate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 != 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())
|
||||
}
|
||||
|
||||
// GenerateServerPath creates the config path based on the service name
|
||||
func (s *Server) GenerateServerPath(steamCMDPath string) string {
|
||||
// Ensure service name is set
|
||||
if s.ServiceName == "" {
|
||||
s.ServiceName = s.GenerateServiceName()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ const (
|
||||
StatusRunning
|
||||
)
|
||||
|
||||
// String converts the ServiceStatus to its string representation
|
||||
func (s ServiceStatus) String() string {
|
||||
switch s {
|
||||
case StatusRunning:
|
||||
@@ -35,7 +34,6 @@ func (s ServiceStatus) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// ParseServiceStatus converts a string to ServiceStatus
|
||||
func ParseServiceStatus(s string) ServiceStatus {
|
||||
switch s {
|
||||
case "SERVICE_RUNNING":
|
||||
@@ -53,31 +51,24 @@ func ParseServiceStatus(s string) ServiceStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler interface
|
||||
func (s ServiceStatus) MarshalJSON() ([]byte, error) {
|
||||
// Return the numeric value instead of string
|
||||
return []byte(strconv.Itoa(int(s))), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler interface
|
||||
func (s *ServiceStatus) UnmarshalJSON(data []byte) error {
|
||||
// Try to parse as number first
|
||||
if i, err := strconv.Atoi(string(data)); err == nil {
|
||||
*s = ServiceStatus(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback to string parsing for backward compatibility
|
||||
str := string(data)
|
||||
if len(str) >= 2 {
|
||||
// Remove quotes if present
|
||||
str = str[1 : len(str)-1]
|
||||
}
|
||||
*s = ParseServiceStatus(str)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface
|
||||
func (s *ServiceStatus) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*s = StatusUnknown
|
||||
@@ -99,7 +90,6 @@ func (s *ServiceStatus) Scan(value interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface
|
||||
func (s ServiceStatus) Value() (driver.Value, error) {
|
||||
return s.String(), nil
|
||||
}
|
||||
|
||||
@@ -10,27 +10,22 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StateHistoryFilter combines common filter capabilities
|
||||
type StateHistoryFilter struct {
|
||||
ServerBasedFilter // Adds server ID from path parameter
|
||||
DateRangeFilter // Adds date range filtering
|
||||
ServerBasedFilter
|
||||
DateRangeFilter
|
||||
|
||||
// Additional fields specific to state history
|
||||
Session TrackSession `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 != "" {
|
||||
if serverUUID, err := uuid.Parse(f.ServerID); err == nil {
|
||||
query = query.Where("server_id = ?", serverUUID)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply date range filter if set
|
||||
timeZero := time.Time{}
|
||||
if f.StartDate != timeZero {
|
||||
query = query.Where("date_created >= ?", f.StartDate)
|
||||
@@ -39,12 +34,10 @@ func (f *StateHistoryFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
|
||||
query = query.Where("date_created <= ?", f.EndDate)
|
||||
}
|
||||
|
||||
// Apply session filter if set
|
||||
if f.Session != "" {
|
||||
query = query.Where("session = ?", f.Session)
|
||||
}
|
||||
|
||||
// Apply player count filters if set
|
||||
if f.MinPlayers != nil {
|
||||
query = query.Where("player_count >= ?", *f.MinPlayers)
|
||||
}
|
||||
@@ -114,10 +107,9 @@ type StateHistory struct {
|
||||
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
|
||||
SessionID uuid.UUID `json:"sessionId" gorm:"not null;type:uuid"`
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -21,7 +21,7 @@ type StateHistoryStats struct {
|
||||
AveragePlayers float64 `json:"averagePlayers"`
|
||||
PeakPlayers int `json:"peakPlayers"`
|
||||
TotalSessions int `json:"totalSessions"`
|
||||
TotalPlaytime int `json:"totalPlaytime" gorm:"-"` // in minutes
|
||||
TotalPlaytime int `json:"totalPlaytime" gorm:"-"`
|
||||
PlayerCountOverTime []PlayerCountPoint `json:"playerCountOverTime" gorm:"-"`
|
||||
SessionTypes []SessionCount `json:"sessionTypes" gorm:"-"`
|
||||
DailyActivity []DailyActivity `json:"dailyActivity" gorm:"-"`
|
||||
|
||||
@@ -16,21 +16,18 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SteamCredentials represents stored Steam login credentials
|
||||
type SteamCredentials struct {
|
||||
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
|
||||
Password string `gorm:"not null" json:"-"`
|
||||
DateCreated time.Time `json:"dateCreated"`
|
||||
LastUpdated time.Time `json:"lastUpdated"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for GORM
|
||||
func (SteamCredentials) TableName() string {
|
||||
return "steam_credentials"
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -42,7 +39,6 @@ func (s *SteamCredentials) BeforeCreate(tx *gorm.DB) error {
|
||||
}
|
||||
s.LastUpdated = now
|
||||
|
||||
// Encrypt password before saving
|
||||
encrypted, err := EncryptPassword(s.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -52,11 +48,9 @@ func (s *SteamCredentials) BeforeCreate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate is a GORM hook that runs before updating credentials
|
||||
func (s *SteamCredentials) BeforeUpdate(tx *gorm.DB) error {
|
||||
s.LastUpdated = time.Now().UTC()
|
||||
|
||||
// Only encrypt if password field is being updated
|
||||
if tx.Statement.Changed("Password") {
|
||||
encrypted, err := EncryptPassword(s.Password)
|
||||
if err != nil {
|
||||
@@ -68,9 +62,7 @@ func (s *SteamCredentials) BeforeUpdate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterFind is a GORM hook that runs after fetching credentials
|
||||
func (s *SteamCredentials) AfterFind(tx *gorm.DB) error {
|
||||
// Decrypt password after fetching
|
||||
if s.Password != "" {
|
||||
decrypted, err := DecryptPassword(s.Password)
|
||||
if err != nil {
|
||||
@@ -81,18 +73,15 @@ func (s *SteamCredentials) AfterFind(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the credentials are valid with enhanced security checks
|
||||
func (s *SteamCredentials) Validate() error {
|
||||
if s.Username == "" {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
|
||||
// Enhanced username validation
|
||||
if len(s.Username) < 3 || len(s.Username) > 64 {
|
||||
return errors.New("username must be between 3 and 64 characters")
|
||||
}
|
||||
|
||||
// Check for valid characters in username (alphanumeric, underscore, hyphen)
|
||||
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, s.Username); !matched {
|
||||
return errors.New("username contains invalid characters")
|
||||
}
|
||||
@@ -101,7 +90,6 @@ func (s *SteamCredentials) Validate() error {
|
||||
return errors.New("password is required")
|
||||
}
|
||||
|
||||
// Basic password validation
|
||||
if len(s.Password) < 6 {
|
||||
return errors.New("password must be at least 6 characters long")
|
||||
}
|
||||
@@ -110,7 +98,6 @@ func (s *SteamCredentials) Validate() error {
|
||||
return errors.New("password is too long")
|
||||
}
|
||||
|
||||
// Check for obvious weak passwords
|
||||
weakPasswords := []string{"password", "123456", "steam", "admin", "user"}
|
||||
lowerPass := strings.ToLower(s.Password)
|
||||
for _, weak := range weakPasswords {
|
||||
@@ -122,8 +109,6 @@ func (s *SteamCredentials) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEncryptionKey returns the encryption key from config.
|
||||
// The key is loaded from the ENCRYPTION_KEY environment variable.
|
||||
func GetEncryptionKey() []byte {
|
||||
key := []byte(configs.EncryptionKey)
|
||||
if len(key) != 32 {
|
||||
@@ -132,7 +117,6 @@ func GetEncryptionKey() []byte {
|
||||
return key
|
||||
}
|
||||
|
||||
// EncryptPassword encrypts a password using AES-256-GCM with enhanced security
|
||||
func EncryptPassword(password string) (string, error) {
|
||||
if password == "" {
|
||||
return "", errors.New("password cannot be empty")
|
||||
@@ -148,33 +132,27 @@ func EncryptPassword(password string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create a new GCM cipher
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create a cryptographically secure nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encrypt the password with authenticated encryption
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(password), nil)
|
||||
|
||||
// Return base64 encoded encrypted password
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// DecryptPassword decrypts an encrypted password with enhanced validation
|
||||
func DecryptPassword(encryptedPassword string) (string, error) {
|
||||
if encryptedPassword == "" {
|
||||
return "", errors.New("encrypted password cannot be empty")
|
||||
}
|
||||
|
||||
// Validate base64 format
|
||||
if len(encryptedPassword) < 24 { // Minimum reasonable length
|
||||
if len(encryptedPassword) < 24 {
|
||||
return "", errors.New("invalid encrypted password format")
|
||||
}
|
||||
|
||||
@@ -184,13 +162,11 @@ func DecryptPassword(encryptedPassword string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create a new GCM cipher
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Decode base64 encoded password
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encryptedPassword)
|
||||
if err != nil {
|
||||
return "", errors.New("invalid base64 encoding")
|
||||
@@ -207,7 +183,6 @@ func DecryptPassword(encryptedPassword string) (string, error) {
|
||||
return "", errors.New("decryption failed - invalid ciphertext or key")
|
||||
}
|
||||
|
||||
// Validate decrypted content
|
||||
decrypted := string(plaintext)
|
||||
if len(decrypted) == 0 || len(decrypted) > 1024 {
|
||||
return "", errors.New("invalid decrypted password")
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User represents a user account in the system.
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
|
||||
Username string `json:"username" gorm:"unique_index;not null"`
|
||||
@@ -17,16 +16,13 @@ type User struct {
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
// BeforeCreate is a GORM hook that runs before creating new users
|
||||
func (s *User) BeforeCreate(tx *gorm.DB) error {
|
||||
s.ID = uuid.New()
|
||||
|
||||
// Validate password strength
|
||||
if err := password.ValidatePasswordStrength(s.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Hash password before saving
|
||||
hashed, err := password.HashPassword(s.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -36,11 +32,8 @@ func (s *User) BeforeCreate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate is a GORM hook that runs before updating users
|
||||
func (s *User) BeforeUpdate(tx *gorm.DB) error {
|
||||
// Only hash if password field is being updated
|
||||
if tx.Statement.Changed("Password") {
|
||||
// Validate password strength
|
||||
if err := password.ValidatePasswordStrength(s.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -55,14 +48,10 @@ func (s *User) BeforeUpdate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterFind is a GORM hook that runs after fetching users
|
||||
func (s *User) AfterFind(tx *gorm.DB) error {
|
||||
// Password remains hashed - never decrypt
|
||||
// This hook is kept for potential future use
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the user data is valid
|
||||
func (s *User) Validate() error {
|
||||
if s.Username == "" {
|
||||
return errors.New("username is required")
|
||||
@@ -73,7 +62,6 @@ func (s *User) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyPassword verifies a plain text password against the stored hash
|
||||
func (s *User) VerifyPassword(plainPassword string) error {
|
||||
return password.VerifyPassword(s.Password, plainPassword)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ServerCreationStep represents the steps in server creation process
|
||||
type ServerCreationStep string
|
||||
|
||||
const (
|
||||
@@ -18,7 +17,6 @@ const (
|
||||
StepCompleted ServerCreationStep = "completed"
|
||||
)
|
||||
|
||||
// StepStatus represents the status of a step
|
||||
type StepStatus string
|
||||
|
||||
const (
|
||||
@@ -28,7 +26,6 @@ const (
|
||||
StatusFailed StepStatus = "failed"
|
||||
)
|
||||
|
||||
// WebSocketMessageType represents different types of WebSocket messages
|
||||
type WebSocketMessageType string
|
||||
|
||||
const (
|
||||
@@ -38,7 +35,6 @@ const (
|
||||
MessageTypeComplete WebSocketMessageType = "complete"
|
||||
)
|
||||
|
||||
// WebSocketMessage is the base structure for all WebSocket messages
|
||||
type WebSocketMessage struct {
|
||||
Type WebSocketMessageType `json:"type"`
|
||||
ServerID *uuid.UUID `json:"server_id,omitempty"`
|
||||
@@ -46,7 +42,6 @@ type WebSocketMessage struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// StepMessage represents a step update message
|
||||
type StepMessage struct {
|
||||
Step ServerCreationStep `json:"step"`
|
||||
Status StepStatus `json:"status"`
|
||||
@@ -54,26 +49,22 @@ type StepMessage struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SteamOutputMessage represents SteamCMD output
|
||||
type SteamOutputMessage struct {
|
||||
Output string `json:"output"`
|
||||
IsError bool `json:"is_error"`
|
||||
}
|
||||
|
||||
// ErrorMessage represents an error message
|
||||
type ErrorMessage struct {
|
||||
Error string `json:"error"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// CompleteMessage represents completion message
|
||||
type CompleteMessage struct {
|
||||
ServerID uuid.UUID `json:"server_id"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// GetStepDescription returns a human-readable description for each step
|
||||
func GetStepDescription(step ServerCreationStep) string {
|
||||
descriptions := map[ServerCreationStep]string{
|
||||
StepValidation: "Validating server configuration",
|
||||
|
||||
Reference in New Issue
Block a user