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" ) // 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) { 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 func (c *Config) IsProductionMode() bool { return c.GetBoolWithDefault(KeyProductionMode, false) } // AllowedOrigins returns the allowed CORS origins func (c *Config) AllowedOrigins() string { return c.GetWithDefault(KeyAllowedOrigins, "") }