471fe76384
- InitCredentialStore nextID defaults to 1 - Auto-migrate existing ID=0 credentials on load - Add public Save() method for migration persistence
255 lines
5.9 KiB
Go
255 lines
5.9 KiB
Go
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)
|
|
}
|
|
|
|
// Check if load triggered ID migration and persist
|
|
CredStore.mu.RLock()
|
|
needsSave := false
|
|
for _, c := range CredStore.data {
|
|
if c.ID == 0 {
|
|
needsSave = true
|
|
break
|
|
}
|
|
}
|
|
CredStore.mu.RUnlock()
|
|
if needsSave {
|
|
if err := CredStore.Save(); err != nil {
|
|
log.Printf("Failed to persist credential migration: %v", 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 = 1
|
|
for _, c := range creds {
|
|
// Auto-migrate legacy ID=0 entries
|
|
if c.ID == 0 {
|
|
c.ID = s.nextID
|
|
s.nextID++
|
|
}
|
|
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)
|
|
}
|
|
|
|
// Save persists credential data to file (public, acquires write lock)
|
|
func (s *CredentialStore) Save() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.save()
|
|
}
|
|
|
|
// 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
|
|
}
|