// Package config предоставляет конфигурацию приложения. // Загружает переменные окружения через godotenv, поддерживает Docker-секреты // через суффикс _FILE (getSecret), сборку DatabaseURL/RedisURL из компонентов. // Экспортирует структуру Config со всеми настройками (БД, S3, JWT, Redis, CORS). package config import ( "os" "strings" "github.com/joho/godotenv" ) type Config struct { AppEnv string ServerPort string DatabaseURL string RedisURL string S3Endpoint string S3PublicEndpoint string S3AccessKey string S3SecretKey string S3Bucket string JWTSecret string JWTRefreshSecret string // CloudPaymentsPublicID и CloudPaymentsAPISecret зарезервированы для будущей интеграции платежей CloudPaymentsPublicID string CloudPaymentsAPISecret string AllowedOrigins []string } func Load() *Config { godotenv.Load() appEnv := getEnv("APP_ENV", "development") isProd := appEnv == "production" cfg := &Config{ AppEnv: appEnv, ServerPort: getEnv("SERVER_PORT", "8080"), DatabaseURL: buildDatabaseURL(), RedisURL: buildRedisURL(), S3Endpoint: getEnv("S3_ENDPOINT", "http://localhost:9000"), S3PublicEndpoint: getEnv("S3_PUBLIC_ENDPOINT", "http://localhost:9000"), S3AccessKey: getEnv("S3_ACCESS_KEY", "photoplaces"), S3SecretKey: getSecret("S3_SECRET_KEY", "photoplaces_dev"), S3Bucket: getEnv("S3_BUCKET", "photoplaces"), JWTSecret: getSecretOrRequire("JWT_SECRET", "dev-secret", isProd), JWTRefreshSecret: getSecretOrRequire("JWT_REFRESH_SECRET", "dev-refresh-secret", isProd), CloudPaymentsPublicID: getEnv("CLOUDPAYMENTS_PUBLIC_ID", ""), CloudPaymentsAPISecret: getEnv("CLOUDPAYMENTS_API_SECRET", ""), AllowedOrigins: parseAllowedOrigins(getEnv("ALLOWED_ORIGINS", ""), isProd), } cfg.validate() return cfg } func getSecretOrRequire(key, fallback string, required bool) string { val := getSecret(key, "") if val == "" { if required { panic("config: " + key + " must be set in production (env var or _FILE)") } return fallback } return val } func parseAllowedOrigins(val string, required bool) []string { if val == "" { if required { panic("config: ALLOWED_ORIGINS must be set in production") } return []string{"http://localhost:3000"} } parts := strings.Split(val, ",") origins := make([]string, 0, len(parts)) for _, p := range parts { if trimmed := strings.TrimSpace(p); trimmed != "" { origins = append(origins, trimmed) } } return origins } func (c *Config) validate() { if c.AppEnv != "production" { return } // Проверяем, что не используются дефолтные dev-значения if c.JWTSecret == "dev-secret" { panic("config validation failed: JWT_SECRET must be set to a secure value in production") } if c.JWTRefreshSecret == "dev-refresh-secret" { panic("config validation failed: JWT_REFRESH_SECRET must be set to a secure value in production") } if len(c.AllowedOrigins) == 1 && c.AllowedOrigins[0] == "http://localhost:3000" { panic("config validation failed: ALLOWED_ORIGINS must be set to production domains") } } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func getSecret(key, fallback string) string { // Try _FILE variant first (Docker secrets) if v := os.Getenv(key + "_FILE"); v != "" { content, err := os.ReadFile(v) if err == nil { return strings.TrimSpace(string(content)) } } // Fallback to env var if v := os.Getenv(key); v != "" { return v } return fallback } func buildDatabaseURL() string { // If DATABASE_URL is explicitly set, use it if v := os.Getenv("DATABASE_URL"); v != "" { return v } // Build from components with secret from file password := getSecret("DB_PASSWORD", "photoplaces_dev") return "postgres://photoplaces:" + password + "@postgres:5432/photoplaces?sslmode=disable" } func buildRedisURL() string { if v := os.Getenv("REDIS_URL"); v != "" { return v } password := getSecret("REDIS_PASSWORD", "") if password != "" { return "redis://:" + password + "@redis:6379/0" } return "redis://redis:6379/0" }