Files
Charon/backend/internal/services/dns_provider_service.go
T
GitHub Actions 9a05e2f927 feat: add DNS provider management features
- Implement DNSProviderCard component for displaying individual DNS provider details.
- Create DNSProviderForm component for adding and editing DNS providers.
- Add DNSProviderSelector component for selecting DNS providers in forms.
- Introduce useDNSProviders hook for fetching and managing DNS provider data.
- Add DNSProviders page for listing and managing DNS providers.
- Update layout to include DNS Providers navigation.
- Enhance UI components with new badge styles and improved layouts.
- Add default provider schemas for various DNS providers.
- Integrate translation strings for DNS provider management.
- Update Vite configuration for improved chunking and performance.
2026-01-02 00:52:37 +00:00

421 lines
13 KiB
Go

package services
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/Wikid82/charon/backend/internal/crypto"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
var (
// ErrDNSProviderNotFound is returned when a DNS provider is not found.
ErrDNSProviderNotFound = errors.New("dns provider not found")
// ErrInvalidProviderType is returned when an unsupported provider type is specified.
ErrInvalidProviderType = errors.New("invalid provider type")
// ErrInvalidCredentials is returned when required credentials are missing.
ErrInvalidCredentials = errors.New("invalid credentials: missing required fields")
// ErrEncryptionFailed is returned when credential encryption fails.
ErrEncryptionFailed = errors.New("failed to encrypt credentials")
// ErrDecryptionFailed is returned when credential decryption fails.
ErrDecryptionFailed = errors.New("failed to decrypt credentials")
)
// SupportedProviderTypes defines the list of supported DNS provider types.
var SupportedProviderTypes = []string{
"cloudflare",
"route53",
"digitalocean",
"googleclouddns",
"namecheap",
"godaddy",
"azure",
"hetzner",
"vultr",
"dnsimple",
}
// ProviderCredentialFields maps provider types to their required credential fields.
var ProviderCredentialFields = map[string][]string{
"cloudflare": {"api_token"},
"route53": {"access_key_id", "secret_access_key", "region"},
"digitalocean": {"auth_token"},
"googleclouddns": {"service_account_json", "project"},
"namecheap": {"api_user", "api_key", "client_ip"},
"godaddy": {"api_key", "api_secret"},
"azure": {"tenant_id", "client_id", "client_secret", "subscription_id", "resource_group"},
"hetzner": {"api_key"},
"vultr": {"api_key"},
"dnsimple": {"oauth_token", "account_id"},
}
// CreateDNSProviderRequest represents the request to create a new DNS provider.
type CreateDNSProviderRequest struct {
Name string `json:"name" binding:"required"`
ProviderType string `json:"provider_type" binding:"required"`
Credentials map[string]string `json:"credentials" binding:"required"`
PropagationTimeout int `json:"propagation_timeout"`
PollingInterval int `json:"polling_interval"`
IsDefault bool `json:"is_default"`
}
// UpdateDNSProviderRequest represents the request to update an existing DNS provider.
type UpdateDNSProviderRequest struct {
Name *string `json:"name"`
Credentials map[string]string `json:"credentials,omitempty"`
PropagationTimeout *int `json:"propagation_timeout"`
PollingInterval *int `json:"polling_interval"`
IsDefault *bool `json:"is_default"`
Enabled *bool `json:"enabled"`
}
// DNSProviderResponse represents the API response for a DNS provider.
type DNSProviderResponse struct {
models.DNSProvider
HasCredentials bool `json:"has_credentials"`
}
// TestResult represents the result of testing DNS provider credentials.
type TestResult struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
Code string `json:"code,omitempty"`
PropagationTimeMs int64 `json:"propagation_time_ms,omitempty"`
}
// DNSProviderService provides operations for managing DNS providers.
type DNSProviderService interface {
List(ctx context.Context) ([]models.DNSProvider, error)
Get(ctx context.Context, id uint) (*models.DNSProvider, error)
Create(ctx context.Context, req CreateDNSProviderRequest) (*models.DNSProvider, error)
Update(ctx context.Context, id uint, req UpdateDNSProviderRequest) (*models.DNSProvider, error)
Delete(ctx context.Context, id uint) error
Test(ctx context.Context, id uint) (*TestResult, error)
TestCredentials(ctx context.Context, req CreateDNSProviderRequest) (*TestResult, error)
GetDecryptedCredentials(ctx context.Context, id uint) (map[string]string, error)
}
// dnsProviderService implements the DNSProviderService interface.
type dnsProviderService struct {
db *gorm.DB
encryptor *crypto.EncryptionService
}
// NewDNSProviderService creates a new DNS provider service.
func NewDNSProviderService(db *gorm.DB, encryptor *crypto.EncryptionService) DNSProviderService {
return &dnsProviderService{
db: db,
encryptor: encryptor,
}
}
// List retrieves all DNS providers.
func (s *dnsProviderService) List(ctx context.Context) ([]models.DNSProvider, error) {
var providers []models.DNSProvider
err := s.db.WithContext(ctx).Order("is_default DESC, name ASC").Find(&providers).Error
return providers, err
}
// Get retrieves a DNS provider by ID.
func (s *dnsProviderService) Get(ctx context.Context, id uint) (*models.DNSProvider, error) {
var provider models.DNSProvider
err := s.db.WithContext(ctx).First(&provider, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrDNSProviderNotFound
}
return nil, err
}
return &provider, nil
}
// Create creates a new DNS provider with encrypted credentials.
func (s *dnsProviderService) Create(ctx context.Context, req CreateDNSProviderRequest) (*models.DNSProvider, error) {
// Validate provider type
if !isValidProviderType(req.ProviderType) {
return nil, ErrInvalidProviderType
}
// Validate required credentials
if err := validateCredentials(req.ProviderType, req.Credentials); err != nil {
return nil, err
}
// Encrypt credentials
credentialsJSON, err := json.Marshal(req.Credentials)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrEncryptionFailed, err)
}
encryptedCreds, err := s.encryptor.Encrypt(credentialsJSON)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrEncryptionFailed, err)
}
// Set defaults
propagationTimeout := req.PropagationTimeout
if propagationTimeout == 0 {
propagationTimeout = 120
}
pollingInterval := req.PollingInterval
if pollingInterval == 0 {
pollingInterval = 5
}
// Handle default provider logic
if req.IsDefault {
// Unset any existing default provider
if err := s.db.WithContext(ctx).Model(&models.DNSProvider{}).Where("is_default = ?", true).Update("is_default", false).Error; err != nil {
return nil, err
}
}
// Create provider
provider := &models.DNSProvider{
UUID: uuid.New().String(),
Name: req.Name,
ProviderType: req.ProviderType,
CredentialsEncrypted: encryptedCreds,
PropagationTimeout: propagationTimeout,
PollingInterval: pollingInterval,
IsDefault: req.IsDefault,
Enabled: true,
}
if err := s.db.WithContext(ctx).Create(provider).Error; err != nil {
return nil, err
}
return provider, nil
}
// Update updates an existing DNS provider.
func (s *dnsProviderService) Update(ctx context.Context, id uint, req UpdateDNSProviderRequest) (*models.DNSProvider, error) {
// Fetch existing provider
provider, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
// Update fields if provided
if req.Name != nil {
provider.Name = *req.Name
}
if req.PropagationTimeout != nil {
provider.PropagationTimeout = *req.PropagationTimeout
}
if req.PollingInterval != nil {
provider.PollingInterval = *req.PollingInterval
}
if req.Enabled != nil {
provider.Enabled = *req.Enabled
}
// Handle credentials update
if req.Credentials != nil && len(req.Credentials) > 0 {
// Validate credentials
if err := validateCredentials(provider.ProviderType, req.Credentials); err != nil {
return nil, err
}
// Encrypt new credentials
credentialsJSON, err := json.Marshal(req.Credentials)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrEncryptionFailed, err)
}
encryptedCreds, err := s.encryptor.Encrypt(credentialsJSON)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrEncryptionFailed, err)
}
provider.CredentialsEncrypted = encryptedCreds
}
// Handle default provider logic
if req.IsDefault != nil && *req.IsDefault {
// Unset any existing default provider
if err := s.db.WithContext(ctx).Model(&models.DNSProvider{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false).Error; err != nil {
return nil, err
}
provider.IsDefault = true
} else if req.IsDefault != nil && !*req.IsDefault {
provider.IsDefault = false
}
// Save updates
if err := s.db.WithContext(ctx).Save(provider).Error; err != nil {
return nil, err
}
return provider, nil
}
// Delete deletes a DNS provider.
func (s *dnsProviderService) Delete(ctx context.Context, id uint) error {
result := s.db.WithContext(ctx).Delete(&models.DNSProvider{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrDNSProviderNotFound
}
return nil
}
// Test tests a saved DNS provider's credentials.
func (s *dnsProviderService) Test(ctx context.Context, id uint) (*TestResult, error) {
provider, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
// Decrypt credentials
credentials, err := s.GetDecryptedCredentials(ctx, id)
if err != nil {
return &TestResult{
Success: false,
Error: "Failed to decrypt credentials",
Code: "DECRYPTION_ERROR",
}, nil
}
// Perform test
result := testDNSProviderCredentials(provider.ProviderType, credentials)
// Update provider statistics
now := time.Now()
provider.LastUsedAt = &now
if result.Success {
provider.SuccessCount++
provider.LastError = ""
} else {
provider.FailureCount++
provider.LastError = result.Error
}
// Save statistics (ignore errors to avoid failing the test operation)
_ = s.db.WithContext(ctx).Save(provider)
return result, nil
}
// TestCredentials tests DNS provider credentials without saving them.
func (s *dnsProviderService) TestCredentials(ctx context.Context, req CreateDNSProviderRequest) (*TestResult, error) {
// Validate provider type
if !isValidProviderType(req.ProviderType) {
return &TestResult{
Success: false,
Error: "Unsupported provider type",
Code: "INVALID_PROVIDER_TYPE",
}, nil
}
// Validate credentials
if err := validateCredentials(req.ProviderType, req.Credentials); err != nil {
return &TestResult{
Success: false,
Error: err.Error(),
Code: "INVALID_CREDENTIALS",
}, nil
}
// Perform test
return testDNSProviderCredentials(req.ProviderType, req.Credentials), nil
}
// GetDecryptedCredentials retrieves and decrypts a DNS provider's credentials.
func (s *dnsProviderService) GetDecryptedCredentials(ctx context.Context, id uint) (map[string]string, error) {
provider, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
// Decrypt credentials
decryptedData, err := s.encryptor.Decrypt(provider.CredentialsEncrypted)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrDecryptionFailed, err)
}
// Parse JSON
var credentials map[string]string
if err := json.Unmarshal(decryptedData, &credentials); err != nil {
return nil, fmt.Errorf("%w: invalid credential format", ErrDecryptionFailed)
}
// Update last used timestamp
now := time.Now()
provider.LastUsedAt = &now
_ = s.db.WithContext(ctx).Save(provider)
return credentials, nil
}
// isValidProviderType checks if a provider type is supported.
func isValidProviderType(providerType string) bool {
for _, supported := range SupportedProviderTypes {
if providerType == supported {
return true
}
}
return false
}
// validateCredentials validates that all required credential fields are present.
func validateCredentials(providerType string, credentials map[string]string) error {
requiredFields, ok := ProviderCredentialFields[providerType]
if !ok {
return ErrInvalidProviderType
}
// Check for required fields
for _, field := range requiredFields {
if value, exists := credentials[field]; !exists || value == "" {
return fmt.Errorf("%w: missing field '%s'", ErrInvalidCredentials, field)
}
}
return nil
}
// testDNSProviderCredentials performs a basic validation test on DNS provider credentials.
// In a real implementation, this would make actual API calls to the DNS provider.
// For now, we simulate the test with basic validation.
func testDNSProviderCredentials(providerType string, credentials map[string]string) *TestResult {
// Simulate validation logic
// In production, this would make actual API calls to verify credentials
startTime := time.Now()
// Basic validation - check if credentials have the expected structure
if err := validateCredentials(providerType, credentials); err != nil {
return &TestResult{
Success: false,
Error: err.Error(),
Code: "VALIDATION_ERROR",
}
}
// Simulate API call delay
elapsed := time.Since(startTime).Milliseconds()
// For now, return success if validation passed
// TODO: Implement actual API calls to DNS providers
return &TestResult{
Success: true,
Message: "DNS provider credentials validated successfully (basic validation only)",
PropagationTimeMs: elapsed,
}
}