package settings import ( "crypto/rand" "encoding/hex" "encoding/json" "sync" "time" "github.com/rose_cat707/luminary/internal/middleware" "github.com/rose_cat707/luminary/internal/model" "gorm.io/gorm" ) // Runtime holds mutable system settings loaded from the database. type Runtime struct { mu sync.RWMutex db *gorm.DB data model.SystemSettings } func NewRuntime(db *gorm.DB) *Runtime { return &Runtime{db: db} } func (r *Runtime) Load() error { var s model.SystemSettings err := r.db.First(&s, 1).Error if err == gorm.ErrRecordNotFound { s = defaultSettings() s = mergeLegacySettings(s) if err := r.db.Create(&s).Error; err != nil { return err } } else if err != nil { return err } r.mu.Lock() r.data = s r.mu.Unlock() r.applySideEffects() return nil } // AfterLoad runs post-bootstrap checks that depend on other tables. func (r *Runtime) AfterLoad() { r.EnsureEncryptionKeyWorks() } func defaultSettings() model.SystemSettings { key := make([]byte, 32) _, _ = rand.Read(key) return model.SystemSettings{ ID: 1, EncryptionKey: hex.EncodeToString(key), SessionTTLSeconds: int64((24 * time.Hour).Seconds()), LoginMaxAttempts: 5, LoginLockoutSeconds: int64((15 * time.Minute).Seconds()), TrustedProxiesJSON: "[]", UsageFlushSeconds: 5, HealthCheckSeconds: 30, GatewayMaxRetries: 3, GatewayRetryBackoffMs: 200, ImageStoragePath: "images", SignedURLTTLSeconds: 3600, PredictionWaitSeconds: 60, } } func (r *Runtime) Snapshot() model.SystemSettings { r.mu.RLock() defer r.mu.RUnlock() return r.data } func (r *Runtime) EncryptionKey() string { r.mu.RLock() defer r.mu.RUnlock() return r.data.EncryptionKey } func (r *Runtime) SessionTTL() time.Duration { r.mu.RLock() defer r.mu.RUnlock() return time.Duration(r.data.SessionTTLSeconds) * time.Second } func (r *Runtime) LoginMaxAttempts() int { r.mu.RLock() defer r.mu.RUnlock() return r.data.LoginMaxAttempts } func (r *Runtime) LoginLockout() time.Duration { r.mu.RLock() defer r.mu.RUnlock() return time.Duration(r.data.LoginLockoutSeconds) * time.Second } func (r *Runtime) TrustedProxies() []string { r.mu.RLock() defer r.mu.RUnlock() var out []string _ = json.Unmarshal([]byte(r.data.TrustedProxiesJSON), &out) return out } func (r *Runtime) UsageFlushInterval() time.Duration { r.mu.RLock() defer r.mu.RUnlock() return time.Duration(r.data.UsageFlushSeconds) * time.Second } func (r *Runtime) HealthCheckInterval() time.Duration { r.mu.RLock() defer r.mu.RUnlock() return time.Duration(r.data.HealthCheckSeconds) * time.Second } func (r *Runtime) GatewayMaxRetries() int { r.mu.RLock() defer r.mu.RUnlock() return r.data.GatewayMaxRetries } func (r *Runtime) GatewayRetryBackoffMs() int { r.mu.RLock() defer r.mu.RUnlock() return r.data.GatewayRetryBackoffMs } func (r *Runtime) ImageStoragePath() string { r.mu.RLock() defer r.mu.RUnlock() if r.data.ImageStoragePath == "" { return "images" } return r.data.ImageStoragePath } func (r *Runtime) PublicBaseURL() string { r.mu.RLock() defer r.mu.RUnlock() return r.data.PublicBaseURL } func (r *Runtime) SignedURLTTL() time.Duration { r.mu.RLock() defer r.mu.RUnlock() sec := r.data.SignedURLTTLSeconds if sec <= 0 { sec = 3600 } return time.Duration(sec) * time.Second } func (r *Runtime) PredictionWaitSeconds() int64 { r.mu.RLock() defer r.mu.RUnlock() if r.data.PredictionWaitSeconds <= 0 { return 60 } return r.data.PredictionWaitSeconds } type UpdateInput struct { EncryptionKey *string `json:"encryption_key"` SessionTTLSeconds *int64 `json:"session_ttl_seconds"` LoginMaxAttempts *int `json:"login_max_attempts"` LoginLockoutSeconds *int64 `json:"login_lockout_seconds"` TrustedProxies []string `json:"trusted_proxies"` UsageFlushSeconds *int64 `json:"usage_flush_seconds"` HealthCheckSeconds *int64 `json:"health_check_seconds"` GatewayMaxRetries *int `json:"gateway_max_retries"` GatewayRetryBackoffMs *int `json:"gateway_retry_backoff_ms"` ImageStoragePath *string `json:"image_storage_path"` PublicBaseURL *string `json:"public_base_url"` SignedURLTTLSeconds *int64 `json:"signed_url_ttl_seconds"` PredictionWaitSeconds *int64 `json:"prediction_wait_seconds"` } func (r *Runtime) Update(in UpdateInput) error { r.mu.Lock() s := r.data if in.EncryptionKey != nil && *in.EncryptionKey != "" { s.EncryptionKey = *in.EncryptionKey } if in.SessionTTLSeconds != nil && *in.SessionTTLSeconds > 0 { s.SessionTTLSeconds = *in.SessionTTLSeconds } if in.LoginMaxAttempts != nil && *in.LoginMaxAttempts > 0 { s.LoginMaxAttempts = *in.LoginMaxAttempts } if in.LoginLockoutSeconds != nil && *in.LoginLockoutSeconds > 0 { s.LoginLockoutSeconds = *in.LoginLockoutSeconds } if in.TrustedProxies != nil { b, _ := json.Marshal(in.TrustedProxies) s.TrustedProxiesJSON = string(b) } if in.UsageFlushSeconds != nil && *in.UsageFlushSeconds > 0 { s.UsageFlushSeconds = *in.UsageFlushSeconds } if in.HealthCheckSeconds != nil && *in.HealthCheckSeconds > 0 { s.HealthCheckSeconds = *in.HealthCheckSeconds } if in.GatewayMaxRetries != nil && *in.GatewayMaxRetries > 0 { s.GatewayMaxRetries = *in.GatewayMaxRetries } if in.GatewayRetryBackoffMs != nil && *in.GatewayRetryBackoffMs >= 0 { s.GatewayRetryBackoffMs = *in.GatewayRetryBackoffMs } if in.ImageStoragePath != nil { s.ImageStoragePath = *in.ImageStoragePath } if in.PublicBaseURL != nil { s.PublicBaseURL = *in.PublicBaseURL } if in.SignedURLTTLSeconds != nil && *in.SignedURLTTLSeconds > 0 { s.SignedURLTTLSeconds = *in.SignedURLTTLSeconds } if in.PredictionWaitSeconds != nil && *in.PredictionWaitSeconds > 0 { s.PredictionWaitSeconds = *in.PredictionWaitSeconds } if err := r.db.Save(&s).Error; err != nil { return err } r.data = s r.mu.Unlock() r.applySideEffects() return nil } func (r *Runtime) applySideEffects() { middleware.ConfigureTrustedProxies(r.TrustedProxies()) } func (r *Runtime) PublicView() map[string]any { s := r.Snapshot() masked := "(已设置)" if s.EncryptionKey == "" { masked = "(未设置)" } var proxies []string _ = json.Unmarshal([]byte(s.TrustedProxiesJSON), &proxies) return map[string]any{ "encryption_key_set": s.EncryptionKey != "", "encryption_key_hint": masked, "session_ttl_seconds": s.SessionTTLSeconds, "login_max_attempts": s.LoginMaxAttempts, "login_lockout_seconds": s.LoginLockoutSeconds, "trusted_proxies": proxies, "usage_flush_seconds": s.UsageFlushSeconds, "health_check_seconds": s.HealthCheckSeconds, "gateway_max_retries": s.GatewayMaxRetries, "gateway_retry_backoff_ms": s.GatewayRetryBackoffMs, "image_storage_path": s.ImageStoragePath, "public_base_url": s.PublicBaseURL, "signed_url_ttl_seconds": s.SignedURLTTLSeconds, "prediction_wait_seconds": s.PredictionWaitSeconds, } }