From c6562d92d6df51649b145f887d9cdf2683306506 Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Fri, 17 Apr 2026 14:52:57 +0530 Subject: [PATCH] refactor(config): split config.go and rename fields to camelCase - Extract postgresPortFromEnv, loadEnvFile, findEnvFile into loader.go - Extract validateDatabaseConfig into validation.go - Rename AppConfig fields from SCREAMING_SNAKE to camelCase (PostgresHost etc.) - Skip .env loading in production (APP_ENV=production) - Harden JWT_SECRET: fatal in production, warn with safe default in dev - Update database/db.go to use renamed AppConfig fields --- backend/internal/config/config.go | 124 +++++--------------------- backend/internal/config/loader.go | 77 ++++++++++++++++ backend/internal/config/validation.go | 28 ++++++ backend/internal/database/db.go | 10 +-- 4 files changed, 131 insertions(+), 108 deletions(-) create mode 100644 backend/internal/config/loader.go create mode 100644 backend/internal/config/validation.go diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index bb2537a..de8f579 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -1,23 +1,16 @@ package config import ( - "errors" - "fmt" "log" "os" - "path/filepath" - "strconv" - "strings" - - "github.com/joho/godotenv" ) type AppConfig struct { - POSTGRES_PASSWORD string - POSTGRES_USER string - POSTGRES_DB string - POSTGRES_HOST string - POSTGRES_PORT int + PostgresPassword string + PostgresUser string + PostgresDB string + PostgresHost string + PostgresPort int } var Config AppConfig @@ -31,12 +24,17 @@ func init() { log.Fatal(err) } + postgresHost := os.Getenv("POSTGRES_HOST") + if postgresHost == "" { + postgresHost = "localhost" + } + Config = AppConfig{ - POSTGRES_PASSWORD: os.Getenv("POSTGRES_PASSWORD"), - POSTGRES_USER: os.Getenv("POSTGRES_USER"), - POSTGRES_DB: os.Getenv("POSTGRES_DB"), - POSTGRES_HOST: os.Getenv("POSTGRES_HOST"), - POSTGRES_PORT: postgresPort, + PostgresPassword: os.Getenv("POSTGRES_PASSWORD"), + PostgresUser: os.Getenv("POSTGRES_USER"), + PostgresDB: os.Getenv("POSTGRES_DB"), + PostgresHost: postgresHost, + PostgresPort: postgresPort, } if err := validateDatabaseConfig(Config); err != nil { @@ -45,93 +43,13 @@ func init() { jwtSecret := os.Getenv("JWT_SECRET") if jwtSecret == "" { - jwtSecret = "secret" - log.Println("WARNING: JWT_SECRET not set or empty; using default insecure secret. Set JWT_SECRET in production.") - } - JWTKey = []byte(jwtSecret) - - log.Println("Configuration loaded successfully.") -} - -func loadEnvFile() { - cwd, err := os.Getwd() - if err != nil { - log.Fatalf("failed to determine current working directory: %v", err) - } - - envPath, err := findEnvFile(cwd) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - log.Println("No .env file found in current or parent directories; using existing environment variables.") - return - } - log.Fatalf("failed to locate .env file: %v", err) - } - - if err := godotenv.Load(envPath); err != nil { - log.Fatalf("failed to load .env file %q: %v", envPath, err) - } - - log.Printf("Loaded environment variables from %s", envPath) -} - -func findEnvFile(startDir string) (string, error) { - dir := startDir - for { - candidate := filepath.Join(dir, ".env") - info, err := os.Stat(candidate) - if err == nil && !info.IsDir() { - return candidate, nil - } - if err != nil && !errors.Is(err, os.ErrNotExist) { - return "", err + if os.Getenv("APP_ENV") == "production" { + log.Fatal("JWT_SECRET must be set in production") } - - parent := filepath.Dir(dir) - if parent == dir { - break - } - dir = parent - } - - return "", os.ErrNotExist -} - -func postgresPortFromEnv() (int, error) { - rawPort := os.Getenv("POSTGRES_PORT") - if rawPort == "" { - return 5432, nil - } - - port, err := strconv.Atoi(rawPort) - if err != nil { - return 0, fmt.Errorf("invalid POSTGRES_PORT %q: must be numeric", rawPort) - } - if port <= 0 { - return 0, fmt.Errorf("invalid POSTGRES_PORT %q: must be greater than 0", rawPort) - } - - return port, nil -} - -func validateDatabaseConfig(cfg AppConfig) error { - missing := make([]string, 0, 4) - - if cfg.POSTGRES_USER == "" { - missing = append(missing, "POSTGRES_USER") - } - if cfg.POSTGRES_PASSWORD == "" { - missing = append(missing, "POSTGRES_PASSWORD") - } - if cfg.POSTGRES_DB == "" { - missing = append(missing, "POSTGRES_DB") - } - if cfg.POSTGRES_HOST == "" { - missing = append(missing, "POSTGRES_HOST") - } - if len(missing) > 0 { - return fmt.Errorf("missing required database config: %s", strings.Join(missing, ", ")) + jwtSecret = "dev-insecure-secret" + log.Println("config: JWT_SECRET unset - insecure default in use. Set JWT_SECRET in production.") } + JWTKey = []byte(jwtSecret) - return nil + log.Println("config: loaded") } diff --git a/backend/internal/config/loader.go b/backend/internal/config/loader.go new file mode 100644 index 0000000..d9806bc --- /dev/null +++ b/backend/internal/config/loader.go @@ -0,0 +1,77 @@ +package config + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "strconv" + + "github.com/joho/godotenv" +) + +func loadEnvFile() { + if os.Getenv("APP_ENV") == "production" { + return + } + + cwd, err := os.Getwd() + if err != nil { + log.Fatalf("failed to determine current working directory: %v", err) + } + + envPath, err := findEnvFile(cwd) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + log.Println("config: no .env file found - environment variables used as-is") + return + } + log.Fatalf("failed to locate .env file: %v", err) + } + + if err := godotenv.Load(envPath); err != nil { + log.Fatalf("failed to load .env file %q: %v", envPath, err) + } + + log.Printf("config: env loaded from %s", envPath) +} + +func findEnvFile(startDir string) (string, error) { + dir := startDir + for { + candidate := filepath.Join(dir, ".env") + info, err := os.Stat(candidate) + if err == nil && !info.IsDir() { + return candidate, nil + } + if err != nil && !errors.Is(err, os.ErrNotExist) { + return "", err + } + + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + + return "", os.ErrNotExist +} + +func postgresPortFromEnv() (int, error) { + rawPort := os.Getenv("POSTGRES_PORT") + if rawPort == "" { + return 5432, nil + } + + port, err := strconv.Atoi(rawPort) + if err != nil { + return 0, fmt.Errorf("invalid POSTGRES_PORT %q: must be numeric", rawPort) + } + if port <= 0 { + return 0, fmt.Errorf("invalid POSTGRES_PORT %q: must be greater than 0", rawPort) + } + + return port, nil +} diff --git a/backend/internal/config/validation.go b/backend/internal/config/validation.go new file mode 100644 index 0000000..8dc124f --- /dev/null +++ b/backend/internal/config/validation.go @@ -0,0 +1,28 @@ +package config + +import ( + "fmt" + "strings" +) + +func validateDatabaseConfig(cfg AppConfig) error { + missing := make([]string, 0, 4) + + if cfg.PostgresUser == "" { + missing = append(missing, "POSTGRES_USER") + } + if cfg.PostgresPassword == "" { + missing = append(missing, "POSTGRES_PASSWORD") + } + if cfg.PostgresDB == "" { + missing = append(missing, "POSTGRES_DB") + } + if cfg.PostgresHost == "" { + missing = append(missing, "POSTGRES_HOST") + } + if len(missing) > 0 { + return fmt.Errorf("missing required database config: %s", strings.Join(missing, ", ")) + } + + return nil +} diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 10fd1e7..edb291e 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -20,11 +20,11 @@ const ( func Connect() error { connStr := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", - config.Config.POSTGRES_HOST, - config.Config.POSTGRES_USER, - config.Config.POSTGRES_PASSWORD, - config.Config.POSTGRES_DB, - config.Config.POSTGRES_PORT, + config.Config.PostgresHost, + config.Config.PostgresUser, + config.Config.PostgresPassword, + config.Config.PostgresDB, + config.Config.PostgresPort, ) var err error