1a69fcfd04
- Updated the installation script to clarify that production wrappers do not include fixed test secrets, improving user guidance for local testing. - Modified the jiggablend-manager and jiggablend-runner scripts to remove fixed test configurations, emphasizing the use of local test credentials via `make init-test`. - Enhanced error handling in the runner script to ensure that an API key is provided, improving security and user feedback. - Added new flags and options for the runner, including sandboxing capabilities and GPU ray tracing control, enhancing flexibility for users. - Improved README documentation to reflect changes in usage and configuration, ensuring users have clear instructions for setup and execution.
364 lines
10 KiB
Go
364 lines
10 KiB
Go
package config
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"jiggablend/internal/database"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
// Config keys stored in database
|
|
const (
|
|
KeyGoogleClientID = "google_client_id"
|
|
KeyGoogleClientSecret = "google_client_secret"
|
|
KeyGoogleRedirectURL = "google_redirect_url"
|
|
KeyDiscordClientID = "discord_client_id"
|
|
KeyDiscordClientSecret = "discord_client_secret"
|
|
KeyDiscordRedirectURL = "discord_redirect_url"
|
|
KeyEnableLocalAuth = "enable_local_auth"
|
|
KeyFixedAPIKey = "fixed_api_key"
|
|
KeyRegistrationEnabled = "registration_enabled"
|
|
KeyProductionMode = "production_mode"
|
|
KeyAllowedOrigins = "allowed_origins"
|
|
KeyFramesPerRenderTask = "frames_per_render_task"
|
|
|
|
// Operational limits (seconds / bytes / counts)
|
|
KeyRenderTimeoutSecs = "render_timeout_seconds"
|
|
KeyEncodeTimeoutSecs = "encode_timeout_seconds"
|
|
KeyMaxUploadBytes = "max_upload_bytes"
|
|
KeySessionCookieMaxAge = "session_cookie_max_age"
|
|
KeyAPIRateLimit = "api_rate_limit"
|
|
KeyAuthRateLimit = "auth_rate_limit"
|
|
)
|
|
|
|
// Config manages application configuration stored in the database
|
|
type Config struct {
|
|
db *database.DB
|
|
}
|
|
|
|
// NewConfig creates a new config manager
|
|
func NewConfig(db *database.DB) *Config {
|
|
return &Config{db: db}
|
|
}
|
|
|
|
// InitializeFromEnv loads configuration from environment variables on first run
|
|
// Environment variables take precedence only if the config key doesn't exist in the database
|
|
// This allows first-run setup via env vars, then subsequent runs use database values
|
|
func (c *Config) InitializeFromEnv() error {
|
|
envMappings := []struct {
|
|
envKey string
|
|
configKey string
|
|
sensitive bool
|
|
}{
|
|
{"GOOGLE_CLIENT_ID", KeyGoogleClientID, false},
|
|
{"GOOGLE_CLIENT_SECRET", KeyGoogleClientSecret, true},
|
|
{"GOOGLE_REDIRECT_URL", KeyGoogleRedirectURL, false},
|
|
{"DISCORD_CLIENT_ID", KeyDiscordClientID, false},
|
|
{"DISCORD_CLIENT_SECRET", KeyDiscordClientSecret, true},
|
|
{"DISCORD_REDIRECT_URL", KeyDiscordRedirectURL, false},
|
|
{"ENABLE_LOCAL_AUTH", KeyEnableLocalAuth, false},
|
|
{"FIXED_API_KEY", KeyFixedAPIKey, true},
|
|
{"PRODUCTION", KeyProductionMode, false},
|
|
{"ALLOWED_ORIGINS", KeyAllowedOrigins, false},
|
|
}
|
|
|
|
for _, mapping := range envMappings {
|
|
envValue := os.Getenv(mapping.envKey)
|
|
if envValue == "" {
|
|
continue
|
|
}
|
|
|
|
// Check if config already exists in database
|
|
exists, err := c.Exists(mapping.configKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check config %s: %w", mapping.configKey, err)
|
|
}
|
|
|
|
if !exists {
|
|
// Store env value in database
|
|
if err := c.Set(mapping.configKey, envValue); err != nil {
|
|
return fmt.Errorf("failed to store config %s: %w", mapping.configKey, err)
|
|
}
|
|
if mapping.sensitive {
|
|
log.Printf("Stored config from env: %s = [REDACTED]", mapping.configKey)
|
|
} else {
|
|
log.Printf("Stored config from env: %s = %s", mapping.configKey, envValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves a config value from the database
|
|
func (c *Config) Get(key string) (string, error) {
|
|
if c == nil || c.db == nil {
|
|
return "", nil
|
|
}
|
|
var value string
|
|
err := c.db.With(func(conn *sql.DB) error {
|
|
return conn.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value)
|
|
})
|
|
if err == sql.ErrNoRows {
|
|
return "", nil
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get config %s: %w", key, err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
// GetWithDefault retrieves a config value or returns a default if not set
|
|
func (c *Config) GetWithDefault(key, defaultValue string) string {
|
|
value, err := c.Get(key)
|
|
if err != nil || value == "" {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
// GetBool retrieves a boolean config value
|
|
func (c *Config) GetBool(key string) (bool, error) {
|
|
value, err := c.Get(key)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return value == "true" || value == "1", nil
|
|
}
|
|
|
|
// GetBoolWithDefault retrieves a boolean config value or returns a default
|
|
func (c *Config) GetBoolWithDefault(key string, defaultValue bool) bool {
|
|
value, err := c.GetBool(key)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
// If the key doesn't exist, Get returns empty string which becomes false
|
|
// Check if key exists to distinguish between "false" and "not set"
|
|
exists, _ := c.Exists(key)
|
|
if !exists {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
// GetInt retrieves an integer config value
|
|
func (c *Config) GetInt(key string) (int, error) {
|
|
value, err := c.Get(key)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if value == "" {
|
|
return 0, nil
|
|
}
|
|
return strconv.Atoi(value)
|
|
}
|
|
|
|
// GetIntWithDefault retrieves an integer config value or returns a default
|
|
func (c *Config) GetIntWithDefault(key string, defaultValue int) int {
|
|
value, err := c.GetInt(key)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
exists, _ := c.Exists(key)
|
|
if !exists {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
// Set stores a config value in the database
|
|
func (c *Config) Set(key, value string) error {
|
|
// Use upsert pattern
|
|
exists, err := c.Exists(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.db.With(func(conn *sql.DB) error {
|
|
if exists {
|
|
_, err = conn.Exec(
|
|
"UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?",
|
|
value, key,
|
|
)
|
|
} else {
|
|
_, err = conn.Exec(
|
|
"INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)",
|
|
key, value,
|
|
)
|
|
}
|
|
return err
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set config %s: %w", key, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetBool stores a boolean config value
|
|
func (c *Config) SetBool(key string, value bool) error {
|
|
strValue := "false"
|
|
if value {
|
|
strValue = "true"
|
|
}
|
|
return c.Set(key, strValue)
|
|
}
|
|
|
|
// SetInt stores an integer config value
|
|
func (c *Config) SetInt(key string, value int) error {
|
|
return c.Set(key, strconv.Itoa(value))
|
|
}
|
|
|
|
// Delete removes a config value from the database
|
|
func (c *Config) Delete(key string) error {
|
|
err := c.db.With(func(conn *sql.DB) error {
|
|
_, err := conn.Exec("DELETE FROM settings WHERE key = ?", key)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete config %s: %w", key, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Exists checks if a config key exists in the database
|
|
func (c *Config) Exists(key string) (bool, error) {
|
|
var exists bool
|
|
err := c.db.With(func(conn *sql.DB) error {
|
|
return conn.QueryRow("SELECT EXISTS(SELECT 1 FROM settings WHERE key = ?)", key).Scan(&exists)
|
|
})
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check config existence %s: %w", key, err)
|
|
}
|
|
return exists, nil
|
|
}
|
|
|
|
// GetAll returns all config values (for debugging/admin purposes)
|
|
func (c *Config) GetAll() (map[string]string, error) {
|
|
var result map[string]string
|
|
err := c.db.With(func(conn *sql.DB) error {
|
|
rows, err := conn.Query("SELECT key, value FROM settings")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get all config: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
result = make(map[string]string)
|
|
for rows.Next() {
|
|
var key, value string
|
|
if err := rows.Scan(&key, &value); err != nil {
|
|
return fmt.Errorf("failed to scan config row: %w", err)
|
|
}
|
|
result[key] = value
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// --- Convenience methods for specific config values ---
|
|
|
|
// GoogleClientID returns the Google OAuth client ID
|
|
func (c *Config) GoogleClientID() string {
|
|
return c.GetWithDefault(KeyGoogleClientID, "")
|
|
}
|
|
|
|
// GoogleClientSecret returns the Google OAuth client secret
|
|
func (c *Config) GoogleClientSecret() string {
|
|
return c.GetWithDefault(KeyGoogleClientSecret, "")
|
|
}
|
|
|
|
// GoogleRedirectURL returns the Google OAuth redirect URL
|
|
func (c *Config) GoogleRedirectURL() string {
|
|
return c.GetWithDefault(KeyGoogleRedirectURL, "")
|
|
}
|
|
|
|
// DiscordClientID returns the Discord OAuth client ID
|
|
func (c *Config) DiscordClientID() string {
|
|
return c.GetWithDefault(KeyDiscordClientID, "")
|
|
}
|
|
|
|
// DiscordClientSecret returns the Discord OAuth client secret
|
|
func (c *Config) DiscordClientSecret() string {
|
|
return c.GetWithDefault(KeyDiscordClientSecret, "")
|
|
}
|
|
|
|
// DiscordRedirectURL returns the Discord OAuth redirect URL
|
|
func (c *Config) DiscordRedirectURL() string {
|
|
return c.GetWithDefault(KeyDiscordRedirectURL, "")
|
|
}
|
|
|
|
// IsLocalAuthEnabled returns whether local authentication is enabled
|
|
func (c *Config) IsLocalAuthEnabled() bool {
|
|
return c.GetBoolWithDefault(KeyEnableLocalAuth, false)
|
|
}
|
|
|
|
// FixedAPIKey returns the fixed API key for testing
|
|
func (c *Config) FixedAPIKey() string {
|
|
return c.GetWithDefault(KeyFixedAPIKey, "")
|
|
}
|
|
|
|
// IsProductionMode returns whether production mode is enabled.
|
|
// True if PRODUCTION=true env is set OR DB setting production_mode is true.
|
|
// This is the single source of truth for Secure cookies, CORS, rate limits, and WS origin.
|
|
func (c *Config) IsProductionMode() bool {
|
|
if os.Getenv("PRODUCTION") == "true" {
|
|
return true
|
|
}
|
|
if c == nil || c.db == nil {
|
|
return false
|
|
}
|
|
return c.GetBoolWithDefault(KeyProductionMode, false)
|
|
}
|
|
|
|
// AllowedOrigins returns the allowed CORS origins
|
|
func (c *Config) AllowedOrigins() string {
|
|
return c.GetWithDefault(KeyAllowedOrigins, "")
|
|
}
|
|
|
|
// GetFramesPerRenderTask returns how many frames to include per render task (min 1, default 1).
|
|
func (c *Config) GetFramesPerRenderTask() int {
|
|
n := c.GetIntWithDefault(KeyFramesPerRenderTask, 1)
|
|
if n < 1 {
|
|
return 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
// RenderTimeoutSeconds returns the per-frame render timeout in seconds (default 3600 = 1 hour).
|
|
func (c *Config) RenderTimeoutSeconds() int {
|
|
return c.GetIntWithDefault(KeyRenderTimeoutSecs, 3600)
|
|
}
|
|
|
|
// EncodeTimeoutSeconds returns the video encode timeout in seconds (default 86400 = 24 hours).
|
|
func (c *Config) EncodeTimeoutSeconds() int {
|
|
return c.GetIntWithDefault(KeyEncodeTimeoutSecs, 86400)
|
|
}
|
|
|
|
// MaxUploadBytes returns the maximum upload size in bytes (default 50 GB).
|
|
func (c *Config) MaxUploadBytes() int64 {
|
|
v := c.GetIntWithDefault(KeyMaxUploadBytes, 50<<30)
|
|
return int64(v)
|
|
}
|
|
|
|
// SessionCookieMaxAgeSec returns the session cookie max-age in seconds (default 86400 = 24 hours).
|
|
func (c *Config) SessionCookieMaxAgeSec() int {
|
|
return c.GetIntWithDefault(KeySessionCookieMaxAge, 86400)
|
|
}
|
|
|
|
// APIRateLimitPerMinute returns the API rate limit (requests per minute per IP, default 100).
|
|
func (c *Config) APIRateLimitPerMinute() int {
|
|
return c.GetIntWithDefault(KeyAPIRateLimit, 100)
|
|
}
|
|
|
|
// AuthRateLimitPerMinute returns the auth rate limit (requests per minute per IP, default 10).
|
|
func (c *Config) AuthRateLimitPerMinute() int {
|
|
return c.GetIntWithDefault(KeyAuthRateLimit, 10)
|
|
}
|
|
|