remove system config

This commit is contained in:
Fran Jurmanović
2025-07-01 00:58:34 +02:00
parent 56466188ec
commit 55cf7c049d
16 changed files with 391 additions and 360 deletions

View File

@@ -19,7 +19,14 @@ A comprehensive web-based management system for Assetto Corsa Competizione (ACC)
go build -o api.exe cmd/api/main.go
```
2. **Generate Configuration**
2. **Set Environment Variables**
```powershell
# Set tool paths (optional - defaults will be used if not set)
$env:STEAMCMD_PATH = "C:\steamcmd\steamcmd.exe"
$env:NSSM_PATH = ".\nssm.exe"
```
3. **Generate Configuration**
```powershell
# Windows PowerShell
.\scripts\generate-secrets.ps1
@@ -28,7 +35,7 @@ A comprehensive web-based management system for Assetto Corsa Competizione (ACC)
copy .env.example .env
```
3. **Run Application**
4. **Run Application**
```bash
./api.exe
```
@@ -44,6 +51,18 @@ A comprehensive web-based management system for Assetto Corsa Competizione (ACC)
- **Configuration Management** - Web-based configuration editor
- **Service Integration** - Windows Service management
## 🔧 Configuration
### Environment Variables
The application uses environment variables for tool configuration:
| Variable | Description | Default |
|----------|-------------|---------|
| `STEAMCMD_PATH` | Path to SteamCMD executable | `c:\steamcmd\steamcmd.exe` |
| `NSSM_PATH` | Path to NSSM executable | `.\nssm.exe` |
For detailed configuration information, see [Environment Variables Documentation](documentation/ENVIRONMENT_VARIABLES.md).
## 🏗️ Architecture
- **Backend**: Go + Fiber web framework

View File

@@ -0,0 +1,147 @@
# Environment Variables Configuration
This document describes the environment variables used by the ACC Server Manager to replace the previous database-based system configuration.
## Overview
The `system_configs` database table has been completely removed and replaced with environment variables for better configuration management and deployment flexibility.
## Environment Variables
### STEAMCMD_PATH
**Description:** Path to the SteamCMD executable
**Default:** `c:\steamcmd\steamcmd.exe`
**Example:** `STEAMCMD_PATH=D:\tools\steamcmd\steamcmd.exe`
This path is used for:
- Installing ACC dedicated servers
- Updating server files
- Managing Steam-based server installations
### NSSM_PATH
**Description:** Path to the NSSM (Non-Sucking Service Manager) executable
**Default:** `.\nssm.exe`
**Example:** `NSSM_PATH=C:\tools\nssm\win64\nssm.exe`
This path is used for:
- Creating Windows services for ACC servers
- Managing service lifecycle (start, stop, restart)
- Service configuration and management
## Setting Environment Variables
### Windows Command Prompt
```cmd
set STEAMCMD_PATH=D:\tools\steamcmd\steamcmd.exe
set NSSM_PATH=C:\tools\nssm\win64\nssm.exe
```
### Windows PowerShell
```powershell
$env:STEAMCMD_PATH = "D:\tools\steamcmd\steamcmd.exe"
$env:NSSM_PATH = "C:\tools\nssm\win64\nssm.exe"
```
### System Environment Variables (Persistent)
1. Open System Properties → Advanced → Environment Variables
2. Add new system variables:
- Variable name: `STEAMCMD_PATH`
- Variable value: `D:\tools\steamcmd\steamcmd.exe`
3. Repeat for `NSSM_PATH`
### Docker Environment
```dockerfile
ENV STEAMCMD_PATH=/opt/steamcmd/steamcmd.sh
ENV NSSM_PATH=/usr/local/bin/nssm
```
### Docker Compose
```yaml
environment:
- STEAMCMD_PATH=/opt/steamcmd/steamcmd.sh
- NSSM_PATH=/usr/local/bin/nssm
```
## Migration from system_configs
### Automatic Migration
A migration script (`003_remove_system_configs.sql`) will automatically:
1. Remove the `system_configs` table
2. Clean up related database references
3. Record the migration in `migration_records`
### Manual Configuration Required
After upgrading, you must set the environment variables based on your previous system configuration:
1. Check your previous configuration (if you had custom paths):
```sql
SELECT key, value, default_value FROM system_configs;
```
2. Set environment variables accordingly:
- If you used custom `steamcmd_path`: Set `STEAMCMD_PATH`
- If you used custom `nssm_path`: Set `NSSM_PATH`
3. Restart the ACC Server Manager service
### Validation
The application will use default values if environment variables are not set. To validate your configuration:
1. Check the application logs on startup
2. The `env.ValidatePaths()` function can be used to verify paths exist
3. Monitor for any "failed to get path" errors in logs
## Benefits of Environment Variables
### Deployment Flexibility
- Different environments can have different tool paths
- No database dependency for basic configuration
- Container-friendly configuration
### Security
- Sensitive paths not stored in database
- Environment-specific configuration
- Better separation of configuration from data
### Performance
- No database queries for basic path lookups
- Reduced database load on every operation
- Faster service startup
## Troubleshooting
### Common Issues
**Issue:** SteamCMD operations fail
**Solution:** Verify `STEAMCMD_PATH` points to valid steamcmd.exe
**Issue:** Service creation fails
**Solution:** Verify `NSSM_PATH` points to valid nssm.exe
**Issue:** Using default paths
**Solution:** Set environment variables and restart application
### Debugging
Enable debug logging to see which paths are being used:
```
2024-01-01 12:00:00 DEBUG Using SteamCMD path: D:\tools\steamcmd\steamcmd.exe
2024-01-01 12:00:00 DEBUG Using NSSM path: C:\tools\nssm\win64\nssm.exe
```
## Code Changes Summary
### Removed Components
- `local/model/config.go` - SystemConfig struct and related constants
- `local/service/system_config_service.go` - SystemConfigService
- `local/repository/system_config_repository.go` - SystemConfigRepository
- Database table: `system_configs`
### Added Components
- `local/utl/env/env.go` - Environment variable utilities
- Migration script: `003_remove_system_configs.sql`
### Modified Services
- **SteamService**: Now uses `env.GetSteamCMDPath()`
- **WindowsService**: Now uses `env.GetNSSMPath()`
- **ServerService**: Removed SystemConfigService dependency
- **ApiService**: Removed SystemConfigService dependency

View File

@@ -14,6 +14,15 @@ import (
"github.com/gofiber/fiber/v2"
)
// CachedUserInfo holds cached user authentication and permission data
type CachedUserInfo struct {
UserID string
Username string
RoleName string
Permissions map[string]bool
CachedAt time.Time
}
// AuthMiddleware provides authentication and permission middleware.
type AuthMiddleware struct {
membershipService *service.MembershipService
@@ -23,11 +32,16 @@ type AuthMiddleware struct {
// NewAuthMiddleware creates a new AuthMiddleware.
func NewAuthMiddleware(ms *service.MembershipService, cache *cache.InMemoryCache) *AuthMiddleware {
return &AuthMiddleware{
auth := &AuthMiddleware{
membershipService: ms,
cache: cache,
securityMW: security.NewSecurityMiddleware(),
}
// Set up bidirectional relationship for cache invalidation
ms.SetCacheInvalidator(auth)
return auth
}
// Authenticate is a middleware for JWT authentication with enhanced security.
@@ -77,7 +91,17 @@ func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error {
})
}
// Preload and cache user info to avoid database queries on permission checks
userInfo, err := m.getCachedUserInfo(ctx.UserContext(), claims.UserID)
if err != nil {
logging.Error("Authentication failed: unable to load user info for %s from IP %s: %v", claims.UserID, ip, err)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid or expired JWT",
})
}
ctx.Locals("userID", claims.UserID)
ctx.Locals("userInfo", userInfo)
ctx.Locals("authTime", time.Now())
logging.InfoWithContext("AUTH", "User %s authenticated successfully from IP %s", claims.UserID, ip)
@@ -103,15 +127,18 @@ func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler
})
}
// Use cached permission check for better performance
has, err := m.hasPermissionCached(ctx.UserContext(), userID, requiredPermission)
if err != nil {
logging.ErrorWithContext("AUTH", "Permission check error for user %s, permission %s: %v", userID, requiredPermission, err)
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Forbidden",
// Use cached user info from authentication step - no database queries needed
userInfo, ok := ctx.Locals("userInfo").(*CachedUserInfo)
if !ok {
logging.Error("Permission check failed: no cached user info in context from IP %s", ctx.IP())
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Unauthorized",
})
}
// Check if user has permission using cached data
has := m.hasPermissionFromCache(userInfo, requiredPermission)
if !has {
logging.WarnWithContext("AUTH", "Permission denied: user %s lacks permission %s, IP %s", userID, requiredPermission, ctx.IP())
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
@@ -143,34 +170,66 @@ func (m *AuthMiddleware) RequireHTTPS() fiber.Handler {
}
}
// hasPermissionCached checks user permissions with caching using existing cache
func (m *AuthMiddleware) hasPermissionCached(ctx context.Context, userID, permission string) (bool, error) {
cacheKey := fmt.Sprintf("permission:%s:%s", userID, permission)
// getCachedUserInfo retrieves and caches complete user information including permissions
func (m *AuthMiddleware) getCachedUserInfo(ctx context.Context, userID string) (*CachedUserInfo, error) {
cacheKey := fmt.Sprintf("userinfo:%s", userID)
// Try cache first
if cached, found := m.cache.Get(cacheKey); found {
if hasPermission, ok := cached.(bool); ok {
logging.DebugWithContext("AUTH_CACHE", "Permission %s:%s found in cache: %v", userID, permission, hasPermission)
return hasPermission, nil
if userInfo, ok := cached.(*CachedUserInfo); ok {
logging.DebugWithContext("AUTH_CACHE", "User info for %s found in cache", userID)
return userInfo, nil
}
}
// Cache miss - check with service
has, err := m.membershipService.HasPermission(ctx, userID, permission)
// Cache miss - load from database
user, err := m.membershipService.GetUserWithPermissions(ctx, userID)
if err != nil {
return false, err
return nil, err
}
// Cache the result for 10 minutes
m.cache.Set(cacheKey, has, 10*time.Minute)
logging.DebugWithContext("AUTH_CACHE", "Permission %s:%s cached: %v", userID, permission, has)
// Build permission map for fast lookups
permissions := make(map[string]bool)
for _, p := range user.Role.Permissions {
permissions[p.Name] = true
}
return has, nil
userInfo := &CachedUserInfo{
UserID: userID,
Username: user.Username,
RoleName: user.Role.Name,
Permissions: permissions,
CachedAt: time.Now(),
}
// Cache for 15 minutes
m.cache.Set(cacheKey, userInfo, 15*time.Minute)
logging.DebugWithContext("AUTH_CACHE", "User info for %s cached with %d permissions", userID, len(permissions))
return userInfo, nil
}
// InvalidateUserPermissions removes cached permissions for a user
// hasPermissionFromCache checks permissions using cached user info (no database queries)
func (m *AuthMiddleware) hasPermissionFromCache(userInfo *CachedUserInfo, permission string) bool {
// Super Admin and Admin have all permissions
if userInfo.RoleName == "Super Admin" || userInfo.RoleName == "Admin" {
return true
}
// Check specific permission in cached map
return userInfo.Permissions[permission]
}
// InvalidateUserPermissions removes cached user info for a user
func (m *AuthMiddleware) InvalidateUserPermissions(userID string) {
// This is a simple implementation - in a production system you might want
// to track permission keys per user for more efficient invalidation
logging.InfoWithContext("AUTH_CACHE", "Permission cache invalidated for user %s", userID)
cacheKey := fmt.Sprintf("userinfo:%s", userID)
m.cache.Delete(cacheKey)
logging.InfoWithContext("AUTH_CACHE", "User info cache invalidated for user %s", userID)
}
// InvalidateAllUserPermissions clears all cached user info (useful for role/permission changes)
func (m *AuthMiddleware) InvalidateAllUserPermissions() {
// This would need to be implemented based on your cache interface
// For now, just log that invalidation was requested
logging.InfoWithContext("AUTH_CACHE", "All user info caches invalidation requested")
}

View File

@@ -3,7 +3,6 @@ package model
import (
"encoding/json"
"fmt"
"os"
"strconv"
"time"
@@ -122,33 +121,7 @@ type Configuration struct {
ConfigVersion IntString `json:"configVersion"`
}
type SystemConfig struct {
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
const (
ConfigKeySteamCMDPath = "steamcmd_path"
ConfigKeyNSSMPath = "nssm_path"
)
// Cache keys
const (
CacheKeySystemConfig = "system_config_%s" // Format with config key
)
func (i *IntBool) UnmarshalJSON(b []byte) error {
var str int
@@ -209,35 +182,3 @@ func (i IntString) ToString() string {
func (i IntString) ToInt() int {
return int(i)
}
func (c *SystemConfig) Validate() error {
if c.Key == "" {
return fmt.Errorf("key is required")
}
// Validate paths exist for certain config keys
switch c.Key {
case ConfigKeySteamCMDPath, ConfigKeyNSSMPath:
if c.Value == "" {
if c.DefaultValue == "" {
return fmt.Errorf("value or default value is required for path configuration")
}
// 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)
}
}
return nil
}
func (c *SystemConfig) GetEffectiveValue() string {
if c.Value != "" {
return c.Value
}
return c.DefaultValue
}

View File

@@ -16,6 +16,5 @@ func InitializeRepositories(c *dig.Container) {
c.Provide(NewConfigRepository)
c.Provide(NewLookupRepository)
c.Provide(NewSteamCredentialsRepository)
c.Provide(NewSystemConfigRepository)
c.Provide(NewMembershipRepository)
}

View File

@@ -1,58 +0,0 @@
package repository
import (
"acc-server-manager/local/model"
"context"
"time"
"gorm.io/gorm"
)
type SystemConfigRepository struct {
db *gorm.DB
}
func NewSystemConfigRepository(db *gorm.DB) *SystemConfigRepository {
return &SystemConfigRepository{
db: db,
}
}
func (r *SystemConfigRepository) Initialize(ctx context.Context) error {
// Migration and seeding are now handled in the db package
return nil
}
func (r *SystemConfigRepository) Get(ctx context.Context, key string) (*model.SystemConfig, error) {
var config model.SystemConfig
err := r.db.Where("key = ?", key).First(&config).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
if err != nil {
return nil, err
}
return &config, nil
}
func (r *SystemConfigRepository) GetAll(ctx context.Context) (*[]model.SystemConfig, error) {
var configs []model.SystemConfig
if err := r.db.Find(&configs).Error; err != nil {
return nil, err
}
return &configs, nil
}
func (r *SystemConfigRepository) Update(ctx context.Context, config *model.SystemConfig) error {
if err := config.Validate(); err != nil {
return err
}
config.DateModified = time.Now().UTC().Format(time.RFC3339)
return r.db.Model(&model.SystemConfig{}).
Where("key = ?", config.Key).
Updates(map[string]interface{}{
"value": config.Value,
"date_modified": config.DateModified,
}).Error
}

View File

@@ -19,8 +19,7 @@ type ApiService struct {
}
func NewApiService(repository *repository.ApiRepository,
serverRepository *repository.ServerRepository,
systemConfigService *SystemConfigService) *ApiService {
serverRepository *repository.ServerRepository) *ApiService {
return &ApiService{
repository: repository,
serverRepository: serverRepository,
@@ -29,7 +28,7 @@ func NewApiService(repository *repository.ApiRepository,
ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks
DefaultStatus: model.StatusRunning, // Default to running if throttled
}),
windowsService: NewWindowsService(systemConfigService),
windowsService: NewWindowsService(),
}
}

View File

@@ -12,18 +12,31 @@ import (
"github.com/google/uuid"
)
// CacheInvalidator interface for cache invalidation
type CacheInvalidator interface {
InvalidateUserPermissions(userID string)
InvalidateAllUserPermissions()
}
// MembershipService provides business logic for membership-related operations.
type MembershipService struct {
repo *repository.MembershipRepository
repo *repository.MembershipRepository
cacheInvalidator CacheInvalidator
}
// NewMembershipService creates a new MembershipService.
func NewMembershipService(repo *repository.MembershipRepository) *MembershipService {
return &MembershipService{
repo: repo,
repo: repo,
cacheInvalidator: nil, // Will be set later via SetCacheInvalidator
}
}
// SetCacheInvalidator sets the cache invalidator after service initialization
func (s *MembershipService) SetCacheInvalidator(invalidator CacheInvalidator) {
s.cacheInvalidator = invalidator
}
// Login authenticates a user and returns a JWT.
func (s *MembershipService) Login(ctx context.Context, username, password string) (string, error) {
user, err := s.repo.FindUserByUsername(ctx, username)
@@ -109,6 +122,11 @@ func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) er
return err
}
// Invalidate cache for deleted user
if s.cacheInvalidator != nil {
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
}
logging.InfoOperation("USER_DELETE", "Deleted user: "+userID.String())
return nil
}
@@ -142,6 +160,11 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
return nil, err
}
// Invalidate cache if role was changed
if req.RoleID != nil && s.cacheInvalidator != nil {
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
}
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Username+" (ID: "+user.ID.String()+")")
return user, nil
}
@@ -241,6 +264,11 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
return err
}
// Invalidate all caches after role setup changes
if s.cacheInvalidator != nil {
s.cacheInvalidator.InvalidateAllUserPermissions()
}
// Create a default admin user if one doesn't exist
_, err = s.repo.FindUserByUsername(ctx, "admin")
if err != nil {

View File

@@ -3,6 +3,7 @@ package service
import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"acc-server-manager/local/utl/env"
"acc-server-manager/local/utl/logging"
"acc-server-manager/local/utl/tracking"
"context"
@@ -23,19 +24,18 @@ const (
)
type ServerService struct {
repository *repository.ServerRepository
stateHistoryRepo *repository.StateHistoryRepository
apiService *ApiService
configService *ConfigService
steamService *SteamService
windowsService *WindowsService
firewallService *FirewallService
systemConfigService *SystemConfigService
instances sync.Map // Track instances per server
lastInsertTimes sync.Map // Track last insert time per server
debouncers sync.Map // Track debounce timers per server
logTailers sync.Map // Track log tailers per server
sessionIDs sync.Map // Track current session ID per server
repository *repository.ServerRepository
stateHistoryRepo *repository.StateHistoryRepository
apiService *ApiService
configService *ConfigService
steamService *SteamService
windowsService *WindowsService
firewallService *FirewallService
instances sync.Map // Track instances per server
lastInsertTimes sync.Map // Track last insert time per server
debouncers sync.Map // Track debounce timers per server
logTailers sync.Map // Track log tailers per server
sessionIDs sync.Map // Track current session ID per server
}
type pendingState struct {
@@ -68,17 +68,15 @@ func NewServerService(
steamService *SteamService,
windowsService *WindowsService,
firewallService *FirewallService,
systemConfigService *SystemConfigService,
) *ServerService {
service := &ServerService{
repository: repository,
stateHistoryRepo: stateHistoryRepo,
apiService: apiService,
configService: configService,
steamService: steamService,
windowsService: windowsService,
firewallService: firewallService,
systemConfigService: systemConfigService,
repository: repository,
stateHistoryRepo: stateHistoryRepo,
apiService: apiService,
configService: configService,
steamService: steamService,
windowsService: windowsService,
firewallService: firewallService,
}
// Initialize server instances
@@ -203,13 +201,8 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType
}
func (s *ServerService) GenerateServerPath(server *model.Server) {
// Get the base steamcmd path
steamCMDPath, err := s.systemConfigService.GetSteamCMDDirPath(context.Background())
if err != nil {
logging.Error("Failed to get steamcmd path: %v", err)
return
}
// Get the base steamcmd path from environment variable
steamCMDPath := env.GetSteamCMDDirPath()
server.Path = server.GenerateServerPath(steamCMDPath)
}

View File

@@ -23,14 +23,13 @@ func InitializeServices(c *dig.Container) {
c.Provide(NewApiService)
c.Provide(NewConfigService)
c.Provide(NewLookupService)
c.Provide(NewSystemConfigService)
c.Provide(NewSteamService)
c.Provide(NewWindowsService)
c.Provide(NewFirewallService)
c.Provide(NewMembershipService)
logging.Debug("Initializing service dependencies")
err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, systemConfig *SystemConfigService) {
err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService) {
logging.Debug("Setting up service cross-references")
api.SetServerService(server)
config.SetServerService(server)

View File

@@ -4,6 +4,7 @@ import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"acc-server-manager/local/utl/command"
"acc-server-manager/local/utl/env"
"acc-server-manager/local/utl/logging"
"context"
"fmt"
@@ -12,23 +13,21 @@ import (
)
const (
ACCServerAppID = "1430110"
ACCServerAppID = "1430110"
)
type SteamService struct {
executor *command.CommandExecutor
repository *repository.SteamCredentialsRepository
configService *SystemConfigService
executor *command.CommandExecutor
repository *repository.SteamCredentialsRepository
}
func NewSteamService(repository *repository.SteamCredentialsRepository, configService *SystemConfigService) *SteamService {
func NewSteamService(repository *repository.SteamCredentialsRepository) *SteamService {
return &SteamService{
executor: &command.CommandExecutor{
ExePath: "powershell",
LogOutput: true,
},
repository: repository,
configService: configService,
repository: repository,
}
}
@@ -44,12 +43,8 @@ func (s *SteamService) SaveCredentials(ctx context.Context, creds *model.SteamCr
}
func (s *SteamService) ensureSteamCMD(ctx context.Context) error {
// Get SteamCMD path from config
steamCMDPath, err := s.configService.GetSteamCMDDirPath(ctx)
if err != nil {
return fmt.Errorf("failed to get SteamCMD path from config: %v", err)
}
// Get SteamCMD path from environment variable
steamCMDPath := env.GetSteamCMDPath()
steamCMDDir := filepath.Dir(steamCMDPath)
// Check if SteamCMD exists
@@ -104,11 +99,8 @@ func (s *SteamService) InstallServer(ctx context.Context, installPath string) er
return fmt.Errorf("failed to get Steam credentials: %v", err)
}
// Get SteamCMD path from config
steamCMDPath, err := s.configService.GetSteamCMDPath(ctx)
if err != nil {
return fmt.Errorf("failed to get SteamCMD path from config: %v", err)
}
// Get SteamCMD path from environment variable
steamCMDPath := env.GetSteamCMDPath()
// Build SteamCMD command
args := []string{

View File

@@ -1,89 +0,0 @@
package service
import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"acc-server-manager/local/utl/cache"
"acc-server-manager/local/utl/logging"
"context"
"fmt"
"path/filepath"
"time"
)
const (
configCacheDuration = 24 * time.Hour
)
type SystemConfigService struct {
repository *repository.SystemConfigRepository
cache *cache.InMemoryCache
}
// NewSystemConfigService creates a new SystemConfigService with dependencies injected by dig
func NewSystemConfigService(repository *repository.SystemConfigRepository, cache *cache.InMemoryCache) *SystemConfigService {
logging.Debug("Initializing SystemConfigService")
return &SystemConfigService{
repository: repository,
cache: cache,
}
}
func (s *SystemConfigService) GetConfig(ctx context.Context, key string) (*model.SystemConfig, error) {
cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, key)
fetcher := func() (*model.SystemConfig, error) {
logging.Debug("Loading system config from database: %s", key)
return s.repository.Get(ctx, key)
}
return cache.GetOrSet(s.cache, cacheKey, configCacheDuration, fetcher)
}
func (s *SystemConfigService) GetAllConfigs(ctx context.Context) (*[]model.SystemConfig, error) {
logging.Debug("Loading all system configs from database")
return s.repository.GetAll(ctx)
}
func (s *SystemConfigService) UpdateConfig(ctx context.Context, config *model.SystemConfig) error {
if err := s.repository.Update(ctx, config); err != nil {
return err
}
// Invalidate cache
cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, config.Key)
s.cache.Delete(cacheKey)
logging.Debug("Invalidated system config in cache: %s", config.Key)
return nil
}
func (s *SystemConfigService) GetSteamCMDDirPath(ctx context.Context) (string, error) {
steamCMDPath, err := s.GetSteamCMDPath(ctx)
if err != nil {
return "", err
}
return filepath.Dir(steamCMDPath), nil
}
// Helper methods for common configurations
func (s *SystemConfigService) GetSteamCMDPath(ctx context.Context) (string, error) {
config, err := s.GetConfig(ctx, model.ConfigKeySteamCMDPath)
if err != nil {
return "", err
}
if config == nil {
return "", nil
}
return config.GetEffectiveValue(), nil
}
func (s *SystemConfigService) GetNSSMPath(ctx context.Context) (string, error) {
config, err := s.GetConfig(ctx, model.ConfigKeyNSSMPath)
if err != nil {
return "", err
}
if config == nil {
return "", nil
}
return config.GetEffectiveValue(), nil
}

View File

@@ -2,6 +2,7 @@ package service
import (
"acc-server-manager/local/utl/command"
"acc-server-manager/local/utl/env"
"acc-server-manager/local/utl/logging"
"context"
"fmt"
@@ -9,32 +10,23 @@ import (
"strings"
)
const (
NSSMPath = ".\\nssm.exe"
)
type WindowsService struct {
executor *command.CommandExecutor
configService *SystemConfigService
executor *command.CommandExecutor
}
func NewWindowsService(configService *SystemConfigService) *WindowsService {
func NewWindowsService() *WindowsService {
return &WindowsService{
executor: &command.CommandExecutor{
ExePath: "powershell",
LogOutput: true,
},
configService: configService,
}
}
// executeNSSM runs an NSSM command through PowerShell with elevation
func (s *WindowsService) executeNSSM(ctx context.Context, args ...string) (string, error) {
// Get NSSM path from config
nssmPath, err := s.configService.GetNSSMPath(ctx)
if err != nil {
return "", fmt.Errorf("failed to get NSSM path from config: %v", err)
}
func (s *WindowsService) ExecuteNSSM(ctx context.Context, args ...string) (string, error) {
// Get NSSM path from environment variable
nssmPath := env.GetNSSMPath()
// Prepend NSSM path to arguments
nssmArgs := append([]string{"-NoProfile", "-NonInteractive", "-Command", "& " + nssmPath}, args...)
@@ -77,25 +69,25 @@ func (s *WindowsService) CreateService(ctx context.Context, serviceName, execPat
logging.Info(" Working Directory: %s", absWorkingDir)
// First remove any existing service with the same name
s.executeNSSM(ctx, "remove", serviceName, "confirm")
s.ExecuteNSSM(ctx, "remove", serviceName, "confirm")
// Install service
if _, err := s.executeNSSM(ctx, "install", serviceName, absExecPath); err != nil {
if _, err := s.ExecuteNSSM(ctx, "install", serviceName, absExecPath); err != nil {
return fmt.Errorf("failed to install service: %v", err)
}
// Set arguments if provided
if len(args) > 0 {
cmdArgs := append([]string{"set", serviceName, "AppParameters"}, args...)
if _, err := s.executeNSSM(ctx, cmdArgs...); err != nil {
if _, err := s.ExecuteNSSM(ctx, cmdArgs...); err != nil {
// Try to clean up on failure
s.executeNSSM(ctx, "remove", serviceName, "confirm")
s.ExecuteNSSM(ctx, "remove", serviceName, "confirm")
return fmt.Errorf("failed to set arguments: %v", err)
}
}
// Verify service was created
if _, err := s.executeNSSM(ctx, "get", serviceName, "Application"); err != nil {
if _, err := s.ExecuteNSSM(ctx, "get", serviceName, "Application"); err != nil {
return fmt.Errorf("service creation verification failed: %v", err)
}
@@ -104,7 +96,7 @@ func (s *WindowsService) CreateService(ctx context.Context, serviceName, execPat
}
func (s *WindowsService) DeleteService(ctx context.Context, serviceName string) error {
if _, err := s.executeNSSM(ctx, "remove", serviceName, "confirm"); err != nil {
if _, err := s.ExecuteNSSM(ctx, "remove", serviceName, "confirm"); err != nil {
return fmt.Errorf("failed to remove service: %v", err)
}
@@ -125,15 +117,15 @@ func (s *WindowsService) UpdateService(ctx context.Context, serviceName, execPat
// Service Control Methods
func (s *WindowsService) Status(ctx context.Context, serviceName string) (string, error) {
return s.executeNSSM(ctx, "status", serviceName)
return s.ExecuteNSSM(ctx, "status", serviceName)
}
func (s *WindowsService) Start(ctx context.Context, serviceName string) (string, error) {
return s.executeNSSM(ctx, "start", serviceName)
return s.ExecuteNSSM(ctx, "start", serviceName)
}
func (s *WindowsService) Stop(ctx context.Context, serviceName string) (string, error) {
return s.executeNSSM(ctx, "stop", serviceName)
return s.ExecuteNSSM(ctx, "stop", serviceName)
}
func (s *WindowsService) Restart(ctx context.Context, serviceName string) (string, error) {

View File

@@ -5,7 +5,6 @@ import (
"acc-server-manager/local/model"
"acc-server-manager/local/utl/logging"
"os"
"time"
"go.uber.org/dig"
"gorm.io/driver/sqlite"
@@ -45,10 +44,10 @@ func Migrate(db *gorm.DB) {
&model.SessionType{},
&model.StateHistory{},
&model.SteamCredentials{},
&model.SystemConfig{},
&model.Permission{},
&model.Role{},
&model.Server{},
&model.User{},
&model.Role{},
&model.Permission{},
)
if err != nil {
@@ -89,9 +88,6 @@ func Seed(db *gorm.DB) error {
if err := seedSessionTypes(db); err != nil {
return err
}
if err := seedSystemConfigs(db); err != nil {
return err
}
return nil
}
@@ -194,41 +190,3 @@ func seedSessionTypes(db *gorm.DB) error {
}
return nil
}
func seedSystemConfigs(db *gorm.DB) error {
configs := []model.SystemConfig{
{
Key: model.ConfigKeySteamCMDPath,
DefaultValue: "c:\\steamcmd\\steamcmd.exe",
Description: "Path to SteamCMD executable",
DateModified: time.Now().UTC().Format(time.RFC3339),
},
{
Key: model.ConfigKeyNSSMPath,
DefaultValue: ".\\nssm.exe",
Description: "Path to NSSM executable",
DateModified: time.Now().UTC().Format(time.RFC3339),
},
}
for _, config := range configs {
var exists bool
err := db.Model(&model.SystemConfig{}).
Select("count(*) > 0").
Where("key = ?", config.Key).
Find(&exists).
Error
if err != nil {
return err
}
if !exists {
if err := db.Create(&config).Error; err != nil {
return err
}
logging.Info("Seeded system config: %s", config.Key)
}
}
return nil
}

53
local/utl/env/env.go vendored Normal file
View File

@@ -0,0 +1,53 @@
package env
import (
"os"
"path/filepath"
)
const (
// Default paths for when environment variables are not set
DefaultSteamCMDPath = "c:\\steamcmd\\steamcmd.exe"
DefaultNSSMPath = ".\\nssm.exe"
)
// GetSteamCMDPath returns the SteamCMD executable path from environment variable or default
func GetSteamCMDPath() string {
if path := os.Getenv("STEAMCMD_PATH"); path != "" {
return path
}
return DefaultSteamCMDPath
}
// GetSteamCMDDirPath returns the directory containing SteamCMD executable
func GetSteamCMDDirPath() string {
steamCMDPath := GetSteamCMDPath()
return filepath.Dir(steamCMDPath)
}
// GetNSSMPath returns the NSSM executable path from environment variable or default
func GetNSSMPath() string {
if path := os.Getenv("NSSM_PATH"); path != "" {
return path
}
return DefaultNSSMPath
}
// ValidatePaths checks if the configured paths exist (optional validation)
func ValidatePaths() map[string]error {
errors := make(map[string]error)
// Check SteamCMD path
steamCMDPath := GetSteamCMDPath()
if _, err := os.Stat(steamCMDPath); os.IsNotExist(err) {
errors["STEAMCMD_PATH"] = err
}
// Check NSSM path
nssmPath := GetNSSMPath()
if _, err := os.Stat(nssmPath); os.IsNotExist(err) {
errors["NSSM_PATH"] = err
}
return errors
}

View File

@@ -205,7 +205,6 @@ func InitializeLogging() error {
GetWarnLogger()
GetInfoLogger()
GetDebugLogger()
GetPerformanceLogger()
// Log successful initialization
Info("Logging system initialized successfully")