Files
Auto-ssl/backend/config/credential_store.go
T
Your Name 9bae6b1d9b youhua
2026-07-25 00:09:24 +08:00

258 lines
6.1 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: false},
{Key: "cf_email", Label: "Email (Global API Key 方式)", Type: "text", Required: false},
{Key: "cf_api_key", Label: "Global API Key", Type: "password", Required: false},
}
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",
nextID: 1,
}
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
}