package config import ( "errors" "fmt" "strings" "sync" "github.com/joho/godotenv" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) type Config struct { DbURL string Port string Host string JWTIssuer string JWTAudience string JWTPrivateEncKey string OAuthRedirectBase string GoogleClientID string GoogleClientSecret string GithubClientID string GithubClientSecret string UIDev bool Env string Debug bool Swagger bool } var ( once sync.Once cached Config loadErr error ) func Load() (Config, error) { once.Do(func() { _ = godotenv.Load() // Use a private viper to avoid global mutation/races v := viper.New() // Defaults v.SetDefault("bind.address", "127.0.0.1") v.SetDefault("bind.port", "8080") v.SetDefault("database.url", "postgres://user:pass@localhost:5432/db?sslmode=disable") v.SetDefault("ui.dev", false) v.SetDefault("env", "development") v.SetDefault("debug", false) v.SetDefault("swagger", false) // Env setup and binding v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() keys := []string{ "bind.address", "bind.port", "database.url", "jwt.issuer", "jwt.audience", "jwt.private.enc.key", "oauth.redirect.base", "google.client.id", "google.client.secret", "github.client.id", "github.client.secret", "ui.dev", "env", "debug", "swagger", } for _, k := range keys { _ = v.BindEnv(k) } // Build config cfg := Config{ DbURL: v.GetString("database.url"), Port: v.GetString("bind.port"), Host: v.GetString("bind.address"), JWTIssuer: v.GetString("jwt.issuer"), JWTAudience: v.GetString("jwt.audience"), JWTPrivateEncKey: v.GetString("jwt.private.enc.key"), OAuthRedirectBase: v.GetString("oauth.redirect.base"), GoogleClientID: v.GetString("google.client.id"), GoogleClientSecret: v.GetString("google.client.secret"), GithubClientID: v.GetString("github.client.id"), GithubClientSecret: v.GetString("github.client.secret"), UIDev: v.GetBool("ui.dev"), Env: v.GetString("env"), Debug: v.GetBool("debug"), Swagger: v.GetBool("swagger"), } // Validate if err := validateConfig(cfg); err != nil { loadErr = err return } cached = cfg }) return cached, loadErr } func validateConfig(cfg Config) error { var errs []string // Required general settings req := map[string]string{ "jwt.issuer": cfg.JWTIssuer, "jwt.audience": cfg.JWTAudience, "jwt.private.enc.key": cfg.JWTPrivateEncKey, "oauth.redirect.base": cfg.OAuthRedirectBase, } for k, v := range req { if strings.TrimSpace(v) == "" { errs = append(errs, fmt.Sprintf("missing required config key %q (env %s)", k, envNameFromKey(k))) } } // OAuth provider requirements: googleOK := strings.TrimSpace(cfg.GoogleClientID) != "" && strings.TrimSpace(cfg.GoogleClientSecret) != "" githubOK := strings.TrimSpace(cfg.GithubClientID) != "" && strings.TrimSpace(cfg.GithubClientSecret) != "" // If partially configured, report what's missing for each if !googleOK && (cfg.GoogleClientID != "" || cfg.GoogleClientSecret != "") { if cfg.GoogleClientID == "" { errs = append(errs, fmt.Sprintf("google.client.id is missing (env %s) while google.client.secret is set", envNameFromKey("google.client.id"))) } if cfg.GoogleClientSecret == "" { errs = append(errs, fmt.Sprintf("google.client.secret is missing (env %s) while google.client.id is set", envNameFromKey("google.client.secret"))) } } if !githubOK && (cfg.GithubClientID != "" || cfg.GithubClientSecret != "") { if cfg.GithubClientID == "" { errs = append(errs, fmt.Sprintf("github.client.id is missing (env %s) while github.client.secret is set", envNameFromKey("github.client.id"))) } if cfg.GithubClientSecret == "" { errs = append(errs, fmt.Sprintf("github.client.secret is missing (env %s) while github.client.id is set", envNameFromKey("github.client.secret"))) } } // Enforce minimum: at least one full provider if !googleOK && !githubOK { errs = append(errs, "at least one OAuth provider must be fully configured: either Google (google.client.id + google.client.secret) or GitHub (github.client.id + github.client.secret)") } if len(errs) > 0 { return errors.New(strings.Join(errs, "; ")) } return nil } func envNameFromKey(key string) string { return strings.ToUpper(strings.ReplaceAll(key, ".", "_")) } func DebugPrintConfig() { cfg, _ := Load() b, err := yaml.Marshal(cfg) if err != nil { fmt.Println("error marshalling config:", err) return } fmt.Println("Loaded configuration:") fmt.Println(string(b)) } func IsUIDev() bool { cfg, _ := Load() return cfg.UIDev } func IsDev() bool { cfg, _ := Load() return strings.EqualFold(cfg.Env, "development") } func IsDebug() bool { cfg, _ := Load() return cfg.Debug } func IsSwaggerEnabled() bool { cfg, _ := Load() return cfg.Swagger }