// Package config loads and validates server configuration from environment // variables and optional config files. It provides a single Config struct // consumed by all other packages at startup. package config import ( "fmt" "os" "strconv" ) // Config holds all application-level configuration. // All environment variables use the PANGOLIN_ prefix (12-factor app style). type Config struct { // HTTPAddr is the public HTTP API listen address. // Env: PANGOLIN_HTTP_ADDR Default: :8080 HTTPAddr string // AdminAddr is the internal admin gRPC listen address. // Placeholder for task #5. // Env: PANGOLIN_ADMIN_ADDR Default: :9090 AdminAddr string // GRPCAddr is the data-plane gRPC listen address. // Placeholder for task #7. // Env: PANGOLIN_GRPC_ADDR Default: :9091 GRPCAddr string // MySQL connection. Set either MySQLDSN or the individual fields. // If MySQLDSN is set the individual fields are ignored. MySQLDSN string // PANGOLIN_MYSQL_DSN MySQLHost string // PANGOLIN_MYSQL_HOST (required when no DSN) MySQLPort string // PANGOLIN_MYSQL_PORT Default: 3306 MySQLUser string // PANGOLIN_MYSQL_USER (required when no DSN) MySQLPassword string // PANGOLIN_MYSQL_PASSWORD MySQLDBName string // PANGOLIN_MYSQL_DBNAME (required when no DSN) // RedisAddr is the Redis server address. // Env: PANGOLIN_REDIS_ADDR Default: 127.0.0.1:6379 RedisAddr string // AutoMigrate controls whether database migrations run at startup. // Env: PANGOLIN_AUTO_MIGRATE=true|false Default: false AutoMigrate bool // JWTSecret is the HS256 signing key. // Placeholder for task #2 (auth middleware). // Env: PANGOLIN_JWT_SECRET JWTSecret string // WebhookSecret is the HMAC-SHA256 shared secret for card-store webhooks. // Env: PANGOLIN_WEBHOOK_SECRET WebhookSecret string } // Load reads Config from environment variables. // Returns an error listing any missing required variables. func Load() (Config, error) { c := Config{ HTTPAddr: envOr("PANGOLIN_HTTP_ADDR", ":8080"), AdminAddr: envOr("PANGOLIN_ADMIN_ADDR", ":9090"), GRPCAddr: envOr("PANGOLIN_GRPC_ADDR", ":9091"), MySQLDSN: os.Getenv("PANGOLIN_MYSQL_DSN"), MySQLHost: os.Getenv("PANGOLIN_MYSQL_HOST"), MySQLPort: envOr("PANGOLIN_MYSQL_PORT", "3306"), MySQLUser: os.Getenv("PANGOLIN_MYSQL_USER"), MySQLPassword: os.Getenv("PANGOLIN_MYSQL_PASSWORD"), MySQLDBName: os.Getenv("PANGOLIN_MYSQL_DBNAME"), RedisAddr: envOr("PANGOLIN_REDIS_ADDR", "127.0.0.1:6379"), JWTSecret: os.Getenv("PANGOLIN_JWT_SECRET"), WebhookSecret: os.Getenv("PANGOLIN_WEBHOOK_SECRET"), } if v := os.Getenv("PANGOLIN_AUTO_MIGRATE"); v != "" { b, err := strconv.ParseBool(v) if err != nil { return Config{}, fmt.Errorf("config: PANGOLIN_AUTO_MIGRATE=%q: %w", v, err) } c.AutoMigrate = b } // Validate: either full DSN or individual connection fields must be set. if c.MySQLDSN == "" { var missing []string if c.MySQLHost == "" { missing = append(missing, "PANGOLIN_MYSQL_HOST") } if c.MySQLUser == "" { missing = append(missing, "PANGOLIN_MYSQL_USER") } if c.MySQLDBName == "" { missing = append(missing, "PANGOLIN_MYSQL_DBNAME") } if len(missing) > 0 { return Config{}, fmt.Errorf("config: required env vars not set: %v (or set PANGOLIN_MYSQL_DSN)", missing) } } return c, nil } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def }