remove system config
This commit is contained in:
23
README.md
23
README.md
@@ -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
|
||||
|
||||
147
documentation/ENVIRONMENT_VARIABLES.md
Normal file
147
documentation/ENVIRONMENT_VARIABLES.md
Normal 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
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -16,6 +16,5 @@ func InitializeRepositories(c *dig.Container) {
|
||||
c.Provide(NewConfigRepository)
|
||||
c.Provide(NewLookupRepository)
|
||||
c.Provide(NewSteamCredentialsRepository)
|
||||
c.Provide(NewSystemConfigRepository)
|
||||
c.Provide(NewMembershipRepository)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
@@ -162,4 +154,4 @@ func (s *SteamService) UpdateServer(ctx context.Context, installPath string) err
|
||||
|
||||
func (s *SteamService) UninstallServer(installPath string) error {
|
||||
return os.RemoveAll(installPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,36 +10,27 @@ 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...)
|
||||
|
||||
|
||||
output, err := s.executor.ExecuteWithOutput(nssmArgs...)
|
||||
if err != nil {
|
||||
// Log the full command and error for debugging
|
||||
@@ -51,7 +43,7 @@ func (s *WindowsService) executeNSSM(ctx context.Context, args ...string) (strin
|
||||
cleaned := strings.TrimSpace(strings.ReplaceAll(output, "\x00", ""))
|
||||
// Remove \r\n from status strings
|
||||
cleaned = strings.TrimSuffix(cleaned, "\r\n")
|
||||
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -144,4 +136,4 @@ func (s *WindowsService) Restart(ctx context.Context, serviceName string) (strin
|
||||
|
||||
// Then start it again
|
||||
return s.Start(ctx, serviceName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
53
local/utl/env/env.go
vendored
Normal 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
|
||||
}
|
||||
@@ -205,7 +205,6 @@ func InitializeLogging() error {
|
||||
GetWarnLogger()
|
||||
GetInfoLogger()
|
||||
GetDebugLogger()
|
||||
GetPerformanceLogger()
|
||||
|
||||
// Log successful initialization
|
||||
Info("Logging system initialized successfully")
|
||||
|
||||
Reference in New Issue
Block a user