package config import ( "fmt" "os" "strings" "time" "github.com/spf13/viper" ) // Config holds all configuration for the application type Config struct { Server ServerConfig Database DatabaseConfig Redis RedisConfig Email EmailConfig Push PushConfig Worker WorkerConfig Security SecurityConfig } type ServerConfig struct { Port int Debug bool AllowedHosts []string Timezone string } type DatabaseConfig struct { Host string Port int User string Password string Database string SSLMode string MaxOpenConns int MaxIdleConns int MaxLifetime time.Duration } type RedisConfig struct { URL string Password string DB int } type EmailConfig struct { Host string Port int User string Password string From string UseTLS bool } type PushConfig struct { // Gorush server URL GorushURL string // APNs (iOS) APNSKeyPath string APNSKeyID string APNSTeamID string APNSTopic string APNSSandbox bool // FCM (Android) FCMServerKey string } type WorkerConfig struct { // Scheduled job times (UTC) TaskReminderHour int TaskReminderMinute int OverdueReminderHour int DailyNotifHour int } type SecurityConfig struct { SecretKey string TokenCacheTTL time.Duration PasswordResetExpiry time.Duration ConfirmationExpiry time.Duration MaxPasswordResetRate int // per hour } var cfg *Config // Load reads configuration from environment variables func Load() (*Config, error) { viper.SetEnvPrefix("") viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // Set defaults setDefaults() cfg = &Config{ Server: ServerConfig{ Port: viper.GetInt("PORT"), Debug: viper.GetBool("DEBUG"), AllowedHosts: strings.Split(viper.GetString("ALLOWED_HOSTS"), ","), Timezone: viper.GetString("TIMEZONE"), }, Database: DatabaseConfig{ Host: viper.GetString("DB_HOST"), Port: viper.GetInt("DB_PORT"), User: viper.GetString("POSTGRES_USER"), Password: viper.GetString("POSTGRES_PASSWORD"), Database: viper.GetString("POSTGRES_DB"), SSLMode: viper.GetString("DB_SSLMODE"), MaxOpenConns: viper.GetInt("DB_MAX_OPEN_CONNS"), MaxIdleConns: viper.GetInt("DB_MAX_IDLE_CONNS"), MaxLifetime: viper.GetDuration("DB_MAX_LIFETIME"), }, Redis: RedisConfig{ URL: viper.GetString("REDIS_URL"), Password: viper.GetString("REDIS_PASSWORD"), DB: viper.GetInt("REDIS_DB"), }, Email: EmailConfig{ Host: viper.GetString("EMAIL_HOST"), Port: viper.GetInt("EMAIL_PORT"), User: viper.GetString("EMAIL_HOST_USER"), Password: viper.GetString("EMAIL_HOST_PASSWORD"), From: viper.GetString("DEFAULT_FROM_EMAIL"), UseTLS: viper.GetBool("EMAIL_USE_TLS"), }, Push: PushConfig{ GorushURL: viper.GetString("GORUSH_URL"), APNSKeyPath: viper.GetString("APNS_AUTH_KEY_PATH"), APNSKeyID: viper.GetString("APNS_AUTH_KEY_ID"), APNSTeamID: viper.GetString("APNS_TEAM_ID"), APNSTopic: viper.GetString("APNS_TOPIC"), APNSSandbox: viper.GetBool("APNS_USE_SANDBOX"), FCMServerKey: viper.GetString("FCM_SERVER_KEY"), }, Worker: WorkerConfig{ TaskReminderHour: viper.GetInt("CELERY_BEAT_REMINDER_HOUR"), TaskReminderMinute: viper.GetInt("CELERY_BEAT_REMINDER_MINUTE"), OverdueReminderHour: 9, // 9:00 AM UTC DailyNotifHour: 11, // 11:00 AM UTC }, Security: SecurityConfig{ SecretKey: viper.GetString("SECRET_KEY"), TokenCacheTTL: 5 * time.Minute, PasswordResetExpiry: 15 * time.Minute, ConfirmationExpiry: 24 * time.Hour, MaxPasswordResetRate: 3, }, } // Validate required fields if err := validate(cfg); err != nil { return nil, err } return cfg, nil } // Get returns the current configuration func Get() *Config { return cfg } func setDefaults() { // Server defaults viper.SetDefault("PORT", 8000) viper.SetDefault("DEBUG", false) viper.SetDefault("ALLOWED_HOSTS", "localhost,127.0.0.1") viper.SetDefault("TIMEZONE", "UTC") // Database defaults viper.SetDefault("DB_HOST", "localhost") viper.SetDefault("DB_PORT", 5432) viper.SetDefault("POSTGRES_USER", "postgres") viper.SetDefault("POSTGRES_DB", "mycrib") viper.SetDefault("DB_SSLMODE", "disable") viper.SetDefault("DB_MAX_OPEN_CONNS", 25) viper.SetDefault("DB_MAX_IDLE_CONNS", 10) viper.SetDefault("DB_MAX_LIFETIME", 600*time.Second) // Redis defaults viper.SetDefault("REDIS_URL", "redis://localhost:6379/0") viper.SetDefault("REDIS_DB", 0) // Email defaults viper.SetDefault("EMAIL_HOST", "smtp.gmail.com") viper.SetDefault("EMAIL_PORT", 587) viper.SetDefault("EMAIL_USE_TLS", true) viper.SetDefault("DEFAULT_FROM_EMAIL", "MyCrib ") // Push notification defaults viper.SetDefault("GORUSH_URL", "http://localhost:8088") viper.SetDefault("APNS_TOPIC", "com.example.mycrib") viper.SetDefault("APNS_USE_SANDBOX", true) // Worker defaults viper.SetDefault("CELERY_BEAT_REMINDER_HOUR", 20) viper.SetDefault("CELERY_BEAT_REMINDER_MINUTE", 0) } func validate(cfg *Config) error { if cfg.Security.SecretKey == "" { // In development, use a default key if cfg.Server.Debug { cfg.Security.SecretKey = "development-secret-key-change-in-production" } else { return fmt.Errorf("SECRET_KEY is required in production") } } if cfg.Database.Password == "" && !cfg.Server.Debug { return fmt.Errorf("POSTGRES_PASSWORD is required") } return nil } // DSN returns the database connection string func (d *DatabaseConfig) DSN() string { return fmt.Sprintf( "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", d.Host, d.Port, d.User, d.Password, d.Database, d.SSLMode, ) } // ReadAPNSKey reads the APNs key from file if path is provided func (p *PushConfig) ReadAPNSKey() (string, error) { if p.APNSKeyPath == "" { return "", nil } content, err := os.ReadFile(p.APNSKeyPath) if err != nil { return "", fmt.Errorf("failed to read APNs key: %w", err) } return string(content), nil }