304 lines
8.1 KiB
Go
304 lines
8.1 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"
|
|
)
|
|
|
|
// 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, "")
|
|
}
|
|
|