package config import ( "encoding/json" "log" "os" "sync" "time" ) // CredentialType represents supported DNS provider types type CredentialType string const ( CredentialAliDNS CredentialType = "alidns" CredentialCloudflare CredentialType = "cloudflare" CredentialDNSPod CredentialType = "dnspod" ) // CredentialField describes a single field of a credential type type CredentialField struct { Key string `json:"key"` Label string `json:"label"` Type string `json:"type"` // text, password Required bool `json:"required"` } // CredentialTypeMeta returns field definitions for each provider type func CredentialTypeMeta(ctype CredentialType) []CredentialField { switch ctype { case CredentialAliDNS: return []CredentialField{ {Key: "ali_key", Label: "AccessKey ID", Type: "text", Required: true}, {Key: "ali_secret", Label: "AccessKey Secret", Type: "password", Required: true}, } case CredentialCloudflare: return []CredentialField{ {Key: "cf_api_token", Label: "API Token", Type: "password", Required: true}, } case CredentialDNSPod: return []CredentialField{ {Key: "dnspod_id", Label: "ID", Type: "text", Required: true}, {Key: "dnspod_key", Label: "Key", Type: "password", Required: true}, } default: return nil } } // Credential holds saved DNS provider credentials type Credential struct { ID uint `json:"id"` Name string `json:"name"` // user-friendly name Type CredentialType `json:"type"` // alidns, cloudflare, dnspod Data string `json:"data"` // JSON string of key-value pairs Masked string `json:"masked"` // masked display version of data CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // CredentialStore manages credential persistence type CredentialStore struct { mu sync.RWMutex data map[uint]*Credential path string nextID uint } var CredStore *CredentialStore // InitCredentialStore initializes the credential store func InitCredentialStore(cfg *Config) { CredStore = &CredentialStore{ data: make(map[uint]*Credential), path: cfg.DataDir + "/credentials.json", } if err := CredStore.Load(); err != nil { log.Printf("No existing credential store: %v, starting fresh", err) } log.Println("Credential store initialized successfully") } // Load reads credentials from JSON file func (s *CredentialStore) Load() error { s.mu.Lock() defer s.mu.Unlock() data, err := os.ReadFile(s.path) if err != nil { return err } var creds []*Credential if err := json.Unmarshal(data, &creds); err != nil { return err } s.data = make(map[uint]*Credential) s.nextID = 0 for _, c := range creds { s.data[c.ID] = c if c.ID >= s.nextID { s.nextID = c.ID + 1 } } return nil } func (s *CredentialStore) save() error { creds := make([]*Credential, 0, len(s.data)) for _, c := range s.data { creds = append(creds, c) } data, err := json.MarshalIndent(creds, "", " ") if err != nil { return err } return os.WriteFile(s.path, data, 0600) } // GetAll returns all credentials func (s *CredentialStore) GetAll() []*Credential { s.mu.RLock() defer s.mu.RUnlock() result := make([]*Credential, 0, len(s.data)) for _, c := range s.data { clone := *c clone.Data = "" // don't expose secrets in list result = append(result, &clone) } // Sort by ID descending (newest first) for i := 0; i < len(result)-1; i++ { for j := i + 1; j < len(result); j++ { if result[i].ID < result[j].ID { result[i], result[j] = result[j], result[i] } } } return result } // GetByID returns a credential with secrets (for internal use) func (s *CredentialStore) GetByID(id uint) *Credential { s.mu.RLock() defer s.mu.RUnlock() c := s.data[id] if c == nil { return nil } clone := *c return &clone } // Create adds a new credential func (s *CredentialStore) Create(cred *Credential) error { s.mu.Lock() defer s.mu.Unlock() cred.ID = s.nextID s.nextID++ now := time.Now() cred.CreatedAt = now cred.UpdatedAt = now cred.Masked = maskCredentialData(cred.Type, cred.Data) s.data[cred.ID] = cred return s.save() } // Update modifies an existing credential, keeping data if masked func (s *CredentialStore) Update(cred *Credential) error { s.mu.Lock() defer s.mu.Unlock() existing, ok := s.data[cred.ID] if !ok { return nil } // If data is masked, keep the existing one if cred.Data == "********" { cred.Data = existing.Data } cred.CreatedAt = existing.CreatedAt cred.UpdatedAt = time.Now() cred.Masked = maskCredentialData(cred.Type, cred.Data) s.data[cred.ID] = cred return s.save() } // Delete removes a credential func (s *CredentialStore) Delete(id uint) error { s.mu.Lock() defer s.mu.Unlock() delete(s.data, id) return s.save() } // maskCredentialData creates a masked version for API display func maskCredentialData(ctype CredentialType, dataJSON string) string { var raw map[string]string if err := json.Unmarshal([]byte(dataJSON), &raw); err != nil { return "{}" } masked := make(map[string]string) for k, v := range raw { if len(v) > 4 { masked[k] = v[:4] + "****" } else { masked[k] = "****" } } b, _ := json.Marshal(masked) return string(b) } // GetDataMap parses the credential data JSON into a map func (c *Credential) GetDataMap() map[string]string { result := make(map[string]string) json.Unmarshal([]byte(c.Data), &result) return result }