package config import ( "encoding/json" "log" "os" "sync" "time" ) // NotifyConfig holds notification configuration that can be set via Web UI type NotifyConfig struct { FeishuEnabled bool `json:"feishu_enabled"` FeishuURL string `json:"feishu_url"` SMTPEnabled bool `json:"smtp_enabled"` SMTPHost string `json:"smtp_host"` SMTPPort string `json:"smtp_port"` SMTPUser string `json:"smtp_user"` SMTPPassword string `json:"smtp_password,omitempty"` SMTPFrom string `json:"smtp_from"` NotifyTo string `json:"notify_to"` // Notification thresholds ExpiryDays int `json:"expiry_days"` // Notify when cert expires within this many days // Internal UpdatedAt time.Time `json:"updated_at"` } // DefaultNotifyConfig returns default notification settings func DefaultNotifyConfig() *NotifyConfig { return &NotifyConfig{ FeishuEnabled: false, SMTPEnabled: false, ExpiryDays: 30, } } // NotifyStore manages notification config persistence type NotifyStore struct { mu sync.RWMutex path string Data *NotifyConfig } var NotifyCfg *NotifyStore // InitNotifyStore initializes the notification config store func InitNotifyStore(cfg *Config) { NotifyCfg = &NotifyStore{ path: cfg.DataDir + "/notify.json", Data: DefaultNotifyConfig(), } if err := NotifyCfg.Load(); err != nil { log.Printf("No existing notify config: %v, using defaults", err) } // Environment variables override file config (for backward compatibility) envOverrideNotifyConfig() log.Println("Notify config store initialized successfully") } func envOverrideNotifyConfig() { store := NotifyCfg if url := os.Getenv("FEISHU_WEBHOOK_URL"); url != "" { store.Data.FeishuEnabled = true store.Data.FeishuURL = url } if host := os.Getenv("SMTP_HOST"); host != "" { store.Data.SMTPEnabled = true store.Data.SMTPHost = host } if port := os.Getenv("SMTP_PORT"); port != "" { store.Data.SMTPPort = port } if user := os.Getenv("SMTP_USER"); user != "" { store.Data.SMTPUser = user } if pass := os.Getenv("SMTP_PASSWORD"); pass != "" { store.Data.SMTPPassword = pass } if from := os.Getenv("SMTP_FROM"); from != "" { store.Data.SMTPFrom = from } if to := os.Getenv("NOTIFY_TO"); to != "" { store.Data.NotifyTo = to } } // Load reads notification config from JSON file func (s *NotifyStore) Load() error { s.mu.Lock() defer s.mu.Unlock() data, err := os.ReadFile(s.path) if err != nil { return err } var nc NotifyConfig if err := json.Unmarshal(data, &nc); err != nil { return err } // Keep existing password if not provided in file (security) if nc.SMTPPassword == "" && s.Data.SMTPPassword != "" { nc.SMTPPassword = s.Data.SMTPPassword } s.Data = &nc return nil } // Save persists the notification config to JSON file func (s *NotifyStore) Save() error { s.mu.Lock() defer s.mu.Unlock() s.Data.UpdatedAt = time.Now() data, err := json.MarshalIndent(s.Data, "", " ") if err != nil { return err } return os.WriteFile(s.path, data, 0600) } // Get returns a copy of the current notify config (without password for API responses) func (s *NotifyStore) Get() *NotifyConfig { s.mu.RLock() defer s.mu.RUnlock() // Return a copy nc := *s.Data // Hide password for API responses if nc.SMTPPassword != "" { nc.SMTPPassword = "********" } return &nc } // Update saves new notification config, keeping password if masked func (s *NotifyStore) Update(nc *NotifyConfig) error { s.mu.Lock() defer s.mu.Unlock() // If password is masked, keep the existing one if nc.SMTPPassword == "********" { nc.SMTPPassword = s.Data.SMTPPassword } nc.UpdatedAt = time.Now() s.Data = nc data, err := json.MarshalIndent(s.Data, "", " ") if err != nil { return err } return os.WriteFile(s.path, data, 0600) } // GetConfig returns the raw config (for internal use, with real password) func (s *NotifyStore) GetConfig() *NotifyConfig { s.mu.RLock() defer s.mu.RUnlock() nc := *s.Data return &nc } // IsConfigured returns true if at least one notification channel is configured func (s *NotifyStore) IsConfigured() bool { s.mu.RLock() defer s.mu.RUnlock() if s.Data.FeishuEnabled && s.Data.FeishuURL != "" { return true } if s.Data.SMTPEnabled && s.Data.SMTPHost != "" && s.Data.NotifyTo != "" { return true } // Also check env fallback return false }