package config import ( "crypto/rand" "encoding/base64" "errors" "fmt" "strings" "github.com/spf13/viper" ) // DefaultJWTSecret is a development-only placeholder. Load replaces it with // an ephemeral secret before returning a usable Config. const DefaultJWTSecret = "dev-secret-do-not-use-in-production" // LoadInfo describes runtime decisions made while loading configuration. type LoadInfo struct { // EphemeralJWTSecret is true when a placeholder jwt.secret was replaced // with a random in-memory value for the current process. EphemeralJWTSecret bool } func defaults(v *viper.Viper) { v.SetDefault("server.host", "0.0.0.0") v.SetDefault("server.port", 10086) v.SetDefault("database.driver", "sqlite3") v.SetDefault("database.sqlite.path", "data/mygo.db") v.SetDefault("database.postgres.host", "localhost") v.SetDefault("database.postgres.port", 5432) v.SetDefault("database.postgres.user", "mygo") v.SetDefault("database.postgres.password", "") v.SetDefault("database.postgres.dbname", "mygo") v.SetDefault("database.postgres.sslmode", "disable") v.SetDefault("storage.driver", "local") v.SetDefault("storage.local.path", "data/files") v.SetDefault("jwt.secret", DefaultJWTSecret) v.SetDefault("jwt.access_ttl", "15m") v.SetDefault("jwt.refresh_ttl", "168h") v.SetDefault("log.level", "info") v.SetDefault("log.file_path", "") v.SetDefault("log.file_level", "debug") } func New() *viper.Viper { v := viper.New() v.SetEnvPrefix("MYGO") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() defaults(v) return v } func Load(v *viper.Viper, cfgFile string) (*Config, LoadInfo, error) { if cfgFile != "" { v.SetConfigFile(cfgFile) } else { v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(".") } if err := v.ReadInConfig(); err != nil { var notFound viper.ConfigFileNotFoundError if !errors.As(err, ¬Found) { return nil, LoadInfo{}, fmt.Errorf("read config: %w", err) } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, LoadInfo{}, fmt.Errorf("unmarshal config: %w", err) } info, err := hardenRuntimeSecrets(&cfg) if err != nil { return nil, LoadInfo{}, err } if err := cfg.Validate(); err != nil { return nil, LoadInfo{}, fmt.Errorf("validate config: %w", err) } return &cfg, info, nil } func hardenRuntimeSecrets(cfg *Config) (LoadInfo, error) { if !isWeakJWTSecret(cfg.JWT.Secret) { return LoadInfo{}, nil } secret, err := generateEphemeralJWTSecret() if err != nil { return LoadInfo{}, fmt.Errorf("generate ephemeral jwt secret: %w", err) } cfg.JWT.Secret = secret return LoadInfo{EphemeralJWTSecret: true}, nil } func isWeakJWTSecret(secret string) bool { switch secret { case DefaultJWTSecret, "change-me-in-production", "changeme-in-production": return true default: return false } } func generateEphemeralJWTSecret() (string, error) { var secret [32]byte if _, err := rand.Read(secret[:]); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(secret[:]), nil }