config.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. // Package config предоставляет конфигурацию приложения.
  2. // Загружает переменные окружения через godotenv, поддерживает Docker-секреты
  3. // через суффикс _FILE (getSecret), сборку DatabaseURL/RedisURL из компонентов.
  4. // Экспортирует структуру Config со всеми настройками (БД, S3, JWT, Redis, CORS).
  5. package config
  6. import (
  7. "os"
  8. "strings"
  9. "github.com/joho/godotenv"
  10. )
  11. type Config struct {
  12. AppEnv string
  13. ServerPort string
  14. DatabaseURL string
  15. RedisURL string
  16. S3Endpoint string
  17. S3PublicEndpoint string
  18. S3AccessKey string
  19. S3SecretKey string
  20. S3Bucket string
  21. JWTSecret string
  22. JWTRefreshSecret string
  23. // CloudPaymentsPublicID и CloudPaymentsAPISecret зарезервированы для будущей интеграции платежей
  24. CloudPaymentsPublicID string
  25. CloudPaymentsAPISecret string
  26. AllowedOrigins []string
  27. }
  28. func Load() *Config {
  29. godotenv.Load()
  30. appEnv := getEnv("APP_ENV", "development")
  31. isProd := appEnv == "production"
  32. cfg := &Config{
  33. AppEnv: appEnv,
  34. ServerPort: getEnv("SERVER_PORT", "8080"),
  35. DatabaseURL: buildDatabaseURL(),
  36. RedisURL: buildRedisURL(),
  37. S3Endpoint: getEnv("S3_ENDPOINT", "http://localhost:9000"),
  38. S3PublicEndpoint: getEnv("S3_PUBLIC_ENDPOINT", "http://localhost:9000"),
  39. S3AccessKey: getEnv("S3_ACCESS_KEY", "photoplaces"),
  40. S3SecretKey: getSecret("S3_SECRET_KEY", "photoplaces_dev"),
  41. S3Bucket: getEnv("S3_BUCKET", "photoplaces"),
  42. JWTSecret: getSecretOrRequire("JWT_SECRET", "dev-secret", isProd),
  43. JWTRefreshSecret: getSecretOrRequire("JWT_REFRESH_SECRET", "dev-refresh-secret", isProd),
  44. CloudPaymentsPublicID: getEnv("CLOUDPAYMENTS_PUBLIC_ID", ""),
  45. CloudPaymentsAPISecret: getEnv("CLOUDPAYMENTS_API_SECRET", ""),
  46. AllowedOrigins: parseAllowedOrigins(getEnv("ALLOWED_ORIGINS", ""), isProd),
  47. }
  48. cfg.validate()
  49. return cfg
  50. }
  51. func getSecretOrRequire(key, fallback string, required bool) string {
  52. val := getSecret(key, "")
  53. if val == "" {
  54. if required {
  55. panic("config: " + key + " must be set in production (env var or _FILE)")
  56. }
  57. return fallback
  58. }
  59. return val
  60. }
  61. func parseAllowedOrigins(val string, required bool) []string {
  62. if val == "" {
  63. if required {
  64. panic("config: ALLOWED_ORIGINS must be set in production")
  65. }
  66. return []string{"http://localhost:3000"}
  67. }
  68. return strings.Split(val, ",")
  69. }
  70. func (c *Config) validate() {
  71. if c.AppEnv != "production" {
  72. return
  73. }
  74. // Проверяем, что не используются дефолтные dev-значения
  75. if c.JWTSecret == "dev-secret" {
  76. panic("config validation failed: JWT_SECRET must be set to a secure value in production")
  77. }
  78. if c.JWTRefreshSecret == "dev-refresh-secret" {
  79. panic("config validation failed: JWT_REFRESH_SECRET must be set to a secure value in production")
  80. }
  81. if len(c.AllowedOrigins) == 1 && c.AllowedOrigins[0] == "http://localhost:3000" {
  82. panic("config validation failed: ALLOWED_ORIGINS must be set to production domains")
  83. }
  84. }
  85. func getEnv(key, fallback string) string {
  86. if v := os.Getenv(key); v != "" {
  87. return v
  88. }
  89. return fallback
  90. }
  91. func getSecret(key, fallback string) string {
  92. // Try _FILE variant first (Docker secrets)
  93. if v := os.Getenv(key + "_FILE"); v != "" {
  94. content, err := os.ReadFile(v)
  95. if err == nil {
  96. return strings.TrimSpace(string(content))
  97. }
  98. }
  99. // Fallback to env var
  100. if v := os.Getenv(key); v != "" {
  101. return v
  102. }
  103. return fallback
  104. }
  105. func buildDatabaseURL() string {
  106. // If DATABASE_URL is explicitly set, use it
  107. if v := os.Getenv("DATABASE_URL"); v != "" {
  108. return v
  109. }
  110. // Build from components with secret from file
  111. password := getSecret("DB_PASSWORD", "photoplaces_dev")
  112. return "postgres://photoplaces:" + password + "@postgres:5432/photoplaces?sslmode=disable"
  113. }
  114. func buildRedisURL() string {
  115. if v := os.Getenv("REDIS_URL"); v != "" {
  116. return v
  117. }
  118. password := getSecret("REDIS_PASSWORD", "")
  119. if password != "" {
  120. return "redis://:" + password + "@redis:6379/0"
  121. }
  122. return "redis://redis:6379/0"
  123. }