feat: registry-driven DNS provider type discovery
Phase 1 of Custom DNS Provider Plugin Support: the /api/v1/dns-providers/types endpoint now returns types dynamically from the dnsprovider.Global() registry instead of a hardcoded list. Backend handler queries registry for all provider types, metadata, and fields Response includes is_built_in flag to distinguish plugins from built-ins Frontend types updated with DNSProviderField interface and new response shape Fixed flaky WAF exclusion test (isolated file-based SQLite DB) Updated operator docs for registry-driven discovery and plugin installation Refs: #461
This commit is contained in:
@@ -205,14 +205,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -2,9 +2,11 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -214,211 +216,81 @@ func (h *DNSProviderHandler) TestCredentials(c *gin.Context) {
|
||||
|
||||
// GetTypes handles GET /api/v1/dns-providers/types
|
||||
// Returns the list of supported DNS provider types with their required fields.
|
||||
// Types are sourced from the provider registry (built-in, custom, and external plugins).
|
||||
func (h *DNSProviderHandler) GetTypes(c *gin.Context) {
|
||||
types := []gin.H{
|
||||
{
|
||||
"type": "cloudflare",
|
||||
"name": "Cloudflare",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "api_token",
|
||||
"label": "API Token",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"hint": "Token with Zone:DNS:Edit permissions",
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://developers.cloudflare.com/api/tokens/",
|
||||
},
|
||||
{
|
||||
"type": "route53",
|
||||
"name": "Amazon Route 53",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "access_key_id",
|
||||
"label": "Access Key ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "secret_access_key",
|
||||
"label": "Secret Access Key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "region",
|
||||
"label": "AWS Region",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"default": "us-east-1",
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-routing-traffic.html",
|
||||
},
|
||||
{
|
||||
"type": "digitalocean",
|
||||
"name": "DigitalOcean",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "auth_token",
|
||||
"label": "API Token",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"hint": "Personal Access Token with read/write scope",
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://docs.digitalocean.com/reference/api/api-reference/",
|
||||
},
|
||||
{
|
||||
"type": "googleclouddns",
|
||||
"name": "Google Cloud DNS",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "service_account_json",
|
||||
"label": "Service Account JSON",
|
||||
"type": "textarea",
|
||||
"required": true,
|
||||
"hint": "JSON key file for service account with DNS Administrator role",
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"label": "Project ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://cloud.google.com/dns/docs/",
|
||||
},
|
||||
{
|
||||
"type": "namecheap",
|
||||
"name": "Namecheap",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "api_user",
|
||||
"label": "API Username",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "api_key",
|
||||
"label": "API Key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "client_ip",
|
||||
"label": "Client IP Address",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"hint": "Your server's public IP address (whitelisted in Namecheap)",
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://www.namecheap.com/support/api/intro/",
|
||||
},
|
||||
{
|
||||
"type": "godaddy",
|
||||
"name": "GoDaddy",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "api_key",
|
||||
"label": "API Key",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "api_secret",
|
||||
"label": "API Secret",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://developer.godaddy.com/",
|
||||
},
|
||||
{
|
||||
"type": "azure",
|
||||
"name": "Azure DNS",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"label": "Tenant ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"label": "Client ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "client_secret",
|
||||
"label": "Client Secret",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "subscription_id",
|
||||
"label": "Subscription ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "resource_group",
|
||||
"label": "Resource Group",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://docs.microsoft.com/en-us/azure/dns/",
|
||||
},
|
||||
{
|
||||
"type": "hetzner",
|
||||
"name": "Hetzner",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "api_key",
|
||||
"label": "API Key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://docs.hetzner.com/dns-console/dns/general/dns-overview/",
|
||||
},
|
||||
{
|
||||
"type": "vultr",
|
||||
"name": "Vultr",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "api_key",
|
||||
"label": "API Key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://www.vultr.com/api/",
|
||||
},
|
||||
{
|
||||
"type": "dnsimple",
|
||||
"name": "DNSimple",
|
||||
"fields": []gin.H{
|
||||
{
|
||||
"name": "oauth_token",
|
||||
"label": "OAuth Token",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
},
|
||||
{
|
||||
"name": "account_id",
|
||||
"label": "Account ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
"documentation_url": "https://developer.dnsimple.com/",
|
||||
},
|
||||
// Get all registered providers from the global registry
|
||||
providers := dnsprovider.Global().List()
|
||||
|
||||
// Build response with provider metadata and fields
|
||||
types := make([]gin.H, 0, len(providers))
|
||||
for _, provider := range providers {
|
||||
metadata := provider.Metadata()
|
||||
|
||||
// Combine required and optional fields
|
||||
requiredFields := provider.RequiredCredentialFields()
|
||||
optionalFields := provider.OptionalCredentialFields()
|
||||
|
||||
// Convert fields to response format with required flag
|
||||
fields := make([]gin.H, 0, len(requiredFields)+len(optionalFields))
|
||||
|
||||
for _, f := range requiredFields {
|
||||
field := gin.H{
|
||||
"name": f.Name,
|
||||
"label": f.Label,
|
||||
"type": f.Type,
|
||||
"required": true,
|
||||
}
|
||||
if f.Placeholder != "" {
|
||||
field["placeholder"] = f.Placeholder
|
||||
}
|
||||
if f.Hint != "" {
|
||||
field["hint"] = f.Hint
|
||||
}
|
||||
if len(f.Options) > 0 {
|
||||
field["options"] = f.Options
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
for _, f := range optionalFields {
|
||||
field := gin.H{
|
||||
"name": f.Name,
|
||||
"label": f.Label,
|
||||
"type": f.Type,
|
||||
"required": false,
|
||||
}
|
||||
if f.Placeholder != "" {
|
||||
field["placeholder"] = f.Placeholder
|
||||
}
|
||||
if f.Hint != "" {
|
||||
field["hint"] = f.Hint
|
||||
}
|
||||
if len(f.Options) > 0 {
|
||||
field["options"] = f.Options
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
providerType := gin.H{
|
||||
"type": metadata.Type,
|
||||
"name": metadata.Name,
|
||||
"description": metadata.Description,
|
||||
"is_built_in": metadata.IsBuiltIn,
|
||||
"fields": fields,
|
||||
}
|
||||
|
||||
if metadata.DocumentationURL != "" {
|
||||
providerType["documentation_url"] = metadata.DocumentationURL
|
||||
}
|
||||
|
||||
types = append(types, providerType)
|
||||
}
|
||||
|
||||
// Sort by type for stable, predictable output (registry.List already sorts, but explicit for safety)
|
||||
sort.Slice(types, func(i, j int) bool {
|
||||
return types[i]["type"].(string) < types[j]["type"].(string)
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"types": types,
|
||||
})
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
_ "github.com/Wikid82/charon/backend/pkg/dnsprovider/builtin" // Auto-register DNS providers
|
||||
_ "github.com/Wikid82/charon/backend/pkg/dnsprovider/custom" // Auto-register custom providers (manual)
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -546,41 +548,175 @@ func TestDNSProviderHandler_TestCredentials(t *testing.T) {
|
||||
func TestDNSProviderHandler_GetTypes(t *testing.T) {
|
||||
router, _ := setupDNSProviderTestRouter()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
t.Run("returns registry-driven types", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
types := response["types"].([]interface{})
|
||||
assert.NotEmpty(t, types)
|
||||
types := response["types"].([]interface{})
|
||||
assert.NotEmpty(t, types)
|
||||
|
||||
// Verify structure of first type
|
||||
cloudflare := types[0].(map[string]interface{})
|
||||
assert.Equal(t, "cloudflare", cloudflare["type"])
|
||||
assert.Equal(t, "Cloudflare", cloudflare["name"])
|
||||
assert.NotEmpty(t, cloudflare["fields"])
|
||||
assert.NotEmpty(t, cloudflare["documentation_url"])
|
||||
// Build a map for easier lookup
|
||||
typeMap := make(map[string]map[string]interface{})
|
||||
for _, providerData := range types {
|
||||
typeData := providerData.(map[string]interface{})
|
||||
typeMap[typeData["type"].(string)] = typeData
|
||||
}
|
||||
|
||||
// Verify all expected provider types are present
|
||||
providerTypes := make(map[string]bool)
|
||||
for _, t := range types {
|
||||
typeMap := t.(map[string]interface{})
|
||||
providerTypes[typeMap["type"].(string)] = true
|
||||
}
|
||||
// Verify cloudflare (built-in) is present with correct metadata
|
||||
cloudflare, exists := typeMap["cloudflare"]
|
||||
assert.True(t, exists, "cloudflare provider should exist")
|
||||
if exists {
|
||||
assert.Equal(t, "Cloudflare", cloudflare["name"])
|
||||
assert.Equal(t, true, cloudflare["is_built_in"])
|
||||
assert.NotEmpty(t, cloudflare["description"])
|
||||
assert.NotEmpty(t, cloudflare["documentation_url"])
|
||||
assert.NotEmpty(t, cloudflare["fields"])
|
||||
|
||||
expectedTypes := []string{
|
||||
"cloudflare", "route53", "digitalocean", "googleclouddns",
|
||||
"namecheap", "godaddy", "azure", "hetzner", "vultr", "dnsimple",
|
||||
}
|
||||
// Verify field structure
|
||||
fields := cloudflare["fields"].([]interface{})
|
||||
assert.NotEmpty(t, fields)
|
||||
firstField := fields[0].(map[string]interface{})
|
||||
assert.NotEmpty(t, firstField["name"])
|
||||
assert.NotEmpty(t, firstField["label"])
|
||||
assert.NotEmpty(t, firstField["type"])
|
||||
_, hasRequired := firstField["required"]
|
||||
assert.True(t, hasRequired, "field should have required attribute")
|
||||
}
|
||||
|
||||
for _, expected := range expectedTypes {
|
||||
assert.True(t, providerTypes[expected], "Missing provider type: "+expected)
|
||||
}
|
||||
// Verify manual (custom, non-built-in) is present
|
||||
manual, exists := typeMap["manual"]
|
||||
assert.True(t, exists, "manual provider should exist")
|
||||
if exists {
|
||||
assert.Equal(t, "Manual (No Automation)", manual["name"])
|
||||
assert.Equal(t, false, manual["is_built_in"])
|
||||
assert.NotEmpty(t, manual["description"])
|
||||
}
|
||||
|
||||
// Verify types are sorted alphabetically
|
||||
var typeNames []string
|
||||
for _, providerData := range types {
|
||||
typeData := providerData.(map[string]interface{})
|
||||
typeNames = append(typeNames, typeData["type"].(string))
|
||||
}
|
||||
sortedNames := make([]string, len(typeNames))
|
||||
copy(sortedNames, typeNames)
|
||||
sort.Strings(sortedNames)
|
||||
assert.Equal(t, sortedNames, typeNames, "Types should be sorted alphabetically")
|
||||
})
|
||||
|
||||
t.Run("includes all expected built-in providers", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
types := response["types"].([]interface{})
|
||||
|
||||
// Build type map
|
||||
providerTypes := make(map[string]bool)
|
||||
for _, providerData := range types {
|
||||
typeData := providerData.(map[string]interface{})
|
||||
providerTypes[typeData["type"].(string)] = true
|
||||
}
|
||||
|
||||
// Expected built-in types
|
||||
expectedTypes := []string{
|
||||
"cloudflare", "route53", "digitalocean", "googleclouddns",
|
||||
"namecheap", "godaddy", "azure", "hetzner", "vultr", "dnsimple",
|
||||
}
|
||||
|
||||
for _, expected := range expectedTypes {
|
||||
assert.True(t, providerTypes[expected], "Missing provider type: "+expected)
|
||||
}
|
||||
|
||||
// Verify manual provider is included (custom, not built-in)
|
||||
assert.True(t, providerTypes["manual"], "Missing manual provider type")
|
||||
})
|
||||
|
||||
t.Run("fields include required flag", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
types := response["types"].([]interface{})
|
||||
|
||||
// Find cloudflare to test required fields
|
||||
for _, providerData := range types {
|
||||
typeData := providerData.(map[string]interface{})
|
||||
if typeData["type"] == "cloudflare" {
|
||||
fields := typeData["fields"].([]interface{})
|
||||
// Cloudflare has at least one required field (api_token)
|
||||
foundRequired := false
|
||||
for _, f := range fields {
|
||||
field := f.(map[string]interface{})
|
||||
if field["name"] == "api_token" {
|
||||
assert.Equal(t, true, field["required"])
|
||||
foundRequired = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundRequired, "Cloudflare should have required api_token field")
|
||||
}
|
||||
|
||||
// Manual provider has optional fields
|
||||
if typeData["type"] == "manual" {
|
||||
fields := typeData["fields"].([]interface{})
|
||||
for _, f := range fields {
|
||||
field := f.(map[string]interface{})
|
||||
// Manual provider's fields are optional
|
||||
assert.Equal(t, false, field["required"])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("optional field attributes are included when present", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
types := response["types"].([]interface{})
|
||||
|
||||
// Find a provider with hint/placeholder to verify optional fields
|
||||
for _, providerData := range types {
|
||||
typeData := providerData.(map[string]interface{})
|
||||
if typeData["type"] == "cloudflare" {
|
||||
fields := typeData["fields"].([]interface{})
|
||||
for _, f := range fields {
|
||||
field := f.(map[string]interface{})
|
||||
if field["name"] == "api_token" {
|
||||
// Cloudflare api_token should have hint and/or placeholder
|
||||
_, hasHint := field["hint"]
|
||||
_, hasPlaceholder := field["placeholder"]
|
||||
assert.True(t, hasHint || hasPlaceholder, "api_token field should have hint or placeholder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDNSProviderHandler_CredentialsNeverExposed(t *testing.T) {
|
||||
|
||||
@@ -3,14 +3,21 @@ package handlers
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
@@ -499,7 +506,29 @@ func TestSecurityHandler_DeleteWAFExclusion_NegativeRuleID(t *testing.T) {
|
||||
// Integration test: Full WAF exclusion workflow
|
||||
func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create a temporary file-based SQLite database for complete isolation
|
||||
// This avoids all the shared memory locking issues with in-memory databases
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, fmt.Sprintf("waf_test_%d.db", time.Now().UnixNano()))
|
||||
dsn := fmt.Sprintf("%s?_journal_mode=WAL&_busy_timeout=10000", dbPath)
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
require.NoError(t, err, "failed to open test database")
|
||||
|
||||
// Ensure cleanup
|
||||
t.Cleanup(func() {
|
||||
sqlDB, _ := db.DB()
|
||||
if sqlDB != nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
os.Remove(dbPath)
|
||||
os.Remove(dbPath + "-wal")
|
||||
os.Remove(dbPath + "-shm")
|
||||
})
|
||||
|
||||
// Migrate the required models
|
||||
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{}))
|
||||
|
||||
handler := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
||||
@@ -512,10 +541,12 @@ func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/security/waf/exclusions", http.NoBody)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, http.StatusOK, w.Code, "Step 1: GET should return 200")
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Len(t, resp["exclusions"].([]any), 0)
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp), "Step 1: response should be valid JSON")
|
||||
exclusions, ok := resp["exclusions"].([]any)
|
||||
require.True(t, ok, "Step 1: exclusions should be an array")
|
||||
require.Len(t, exclusions, 0, "Step 1: should start with 0 exclusions")
|
||||
|
||||
// Step 2: Add first exclusion (full rule removal)
|
||||
payload := map[string]any{
|
||||
@@ -527,7 +558,7 @@ func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) {
|
||||
req, _ = http.NewRequest("POST", "/security/waf/exclusions", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, http.StatusOK, w.Code, "Step 2: POST first exclusion should return 200, got: %s", w.Body.String())
|
||||
|
||||
// Step 3: Add second exclusion (targeted)
|
||||
payload = map[string]any{
|
||||
@@ -540,28 +571,33 @@ func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) {
|
||||
req, _ = http.NewRequest("POST", "/security/waf/exclusions", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, http.StatusOK, w.Code, "Step 3: POST second exclusion should return 200, got: %s", w.Body.String())
|
||||
|
||||
// Step 4: Verify both exclusions exist
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/security/waf/exclusions", http.NoBody)
|
||||
router.ServeHTTP(w, req)
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Len(t, resp["exclusions"].([]any), 2)
|
||||
require.Equal(t, http.StatusOK, w.Code, "Step 4: GET should return 200")
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp), "Step 4: response should be valid JSON")
|
||||
exclusions, ok = resp["exclusions"].([]any)
|
||||
require.True(t, ok, "Step 4: exclusions should be an array")
|
||||
require.Len(t, exclusions, 2, "Step 4: should have 2 exclusions after adding 2")
|
||||
|
||||
// Step 5: Delete first exclusion
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE", "/security/waf/exclusions/942100", http.NoBody)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, http.StatusOK, w.Code, "Step 5: DELETE should return 200, got: %s", w.Body.String())
|
||||
|
||||
// Step 6: Verify only second exclusion remains
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/security/waf/exclusions", http.NoBody)
|
||||
router.ServeHTTP(w, req)
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
exclusions := resp["exclusions"].([]any)
|
||||
assert.Len(t, exclusions, 1)
|
||||
require.Equal(t, http.StatusOK, w.Code, "Step 6: GET should return 200")
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp), "Step 6: response should be valid JSON")
|
||||
exclusions, ok = resp["exclusions"].([]any)
|
||||
require.True(t, ok, "Step 6: exclusions should be an array")
|
||||
require.Len(t, exclusions, 1, "Step 6: should have 1 exclusion after deleting 1")
|
||||
first := exclusions[0].(map[string]any)
|
||||
assert.Equal(t, float64(941100), first["rule_id"])
|
||||
assert.Equal(t, "ARGS:content", first["target"])
|
||||
|
||||
@@ -151,6 +151,47 @@ When running Charon in Docker:
|
||||
|
||||
Once a plugin is installed and loaded, it appears in the DNS provider list alongside built-in providers.
|
||||
|
||||
### Discovering Loaded Plugins via API
|
||||
|
||||
Query available provider types to see all registered providers (built-in and plugins):
|
||||
|
||||
```bash
|
||||
curl https://charon.example.com/api/v1/dns-providers/types \
|
||||
-H "Authorization: Bearer YOUR-TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"types": [
|
||||
{
|
||||
"type": "cloudflare",
|
||||
"name": "Cloudflare",
|
||||
"description": "Cloudflare DNS provider",
|
||||
"documentation_url": "https://developers.cloudflare.com/api/",
|
||||
"is_built_in": true,
|
||||
"fields": [...]
|
||||
},
|
||||
{
|
||||
"type": "powerdns",
|
||||
"name": "PowerDNS",
|
||||
"description": "PowerDNS Authoritative Server with HTTP API",
|
||||
"documentation_url": "https://doc.powerdns.com/authoritative/http-api/",
|
||||
"is_built_in": false,
|
||||
"fields": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `is_built_in` | `true` = compiled into Charon, `false` = external plugin |
|
||||
| `fields` | Credential field specifications for the UI form |
|
||||
|
||||
### Via Web UI
|
||||
|
||||
1. Navigate to **Settings** → **DNS Providers**
|
||||
@@ -224,6 +265,19 @@ The plugin automatically configures Caddy's DNS challenge for Let's Encrypt:
|
||||
|
||||
### Listing Loaded Plugins
|
||||
|
||||
**Via Types Endpoint (Recommended):**
|
||||
|
||||
Filter for plugins using `is_built_in: false`:
|
||||
|
||||
```bash
|
||||
curl https://charon.example.com/api/v1/dns-providers/types \
|
||||
-H "Authorization: Bearer YOUR-TOKEN" | jq '.types[] | select(.is_built_in == false)'
|
||||
```
|
||||
|
||||
**Via Plugins Endpoint:**
|
||||
|
||||
Get detailed plugin metadata including version and author:
|
||||
|
||||
```bash
|
||||
curl https://charon.example.com/api/admin/plugins \
|
||||
-H "Authorization: Bearer YOUR-TOKEN"
|
||||
|
||||
@@ -12,7 +12,13 @@ DNS providers enable Charon to obtain SSL/TLS certificates for wildcard domains
|
||||
|
||||
## Supported DNS Providers
|
||||
|
||||
Charon supports the following DNS providers through Caddy's libdns modules:
|
||||
Charon dynamically discovers available DNS provider types from an internal registry. This registry includes:
|
||||
|
||||
- **Built-in providers** — Compiled into Charon (Cloudflare, Route 53, etc.)
|
||||
- **Custom providers** — Special-purpose providers like `manual` for unsupported DNS services
|
||||
- **External plugins** — Third-party `.so` plugin files loaded at runtime
|
||||
|
||||
### Built-in Providers
|
||||
|
||||
| Provider | Type | Setup Guide |
|
||||
|----------|------|-------------|
|
||||
@@ -27,6 +33,84 @@ Charon supports the following DNS providers through Caddy's libdns modules:
|
||||
| Vultr | `vultr` | [Documentation](https://caddyserver.com/docs/modules/dns.providers.vultr) |
|
||||
| DNSimple | `dnsimple` | [Documentation](https://caddyserver.com/docs/modules/dns.providers.dnsimple) |
|
||||
|
||||
### Custom Providers
|
||||
|
||||
| Provider | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| Manual DNS | `manual` | For DNS providers without API support. Displays TXT record for manual creation. |
|
||||
|
||||
### Discovering Available Provider Types
|
||||
|
||||
Query available provider types programmatically via the API:
|
||||
|
||||
```bash
|
||||
curl https://your-charon-instance/api/v1/dns-providers/types \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"types": [
|
||||
{
|
||||
"type": "cloudflare",
|
||||
"name": "Cloudflare",
|
||||
"description": "Cloudflare DNS provider",
|
||||
"documentation_url": "https://developers.cloudflare.com/api/",
|
||||
"is_built_in": true,
|
||||
"fields": [...]
|
||||
},
|
||||
{
|
||||
"type": "manual",
|
||||
"name": "Manual DNS",
|
||||
"description": "Manually create DNS TXT records",
|
||||
"documentation_url": "",
|
||||
"is_built_in": false,
|
||||
"fields": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response fields:**
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `type` | Unique identifier used in API requests |
|
||||
| `name` | Human-readable display name |
|
||||
| `description` | Brief description of the provider |
|
||||
| `documentation_url` | Link to provider's API documentation |
|
||||
| `is_built_in` | `true` for compiled providers, `false` for plugins/custom |
|
||||
| `fields` | Required credential fields and their specifications |
|
||||
|
||||
> **Tip:** Use `is_built_in` to distinguish official providers from external plugins in your automation workflows.
|
||||
|
||||
## Adding External Plugins
|
||||
|
||||
Extend Charon with third-party DNS provider plugins by placing `.so` files in the plugin directory.
|
||||
|
||||
### Installation
|
||||
|
||||
1. Set the plugin directory environment variable:
|
||||
|
||||
```bash
|
||||
export CHARON_PLUGINS_DIR=/etc/charon/plugins
|
||||
```
|
||||
|
||||
2. Copy plugin files:
|
||||
|
||||
```bash
|
||||
cp powerdns.so /etc/charon/plugins/
|
||||
chmod 755 /etc/charon/plugins/powerdns.so
|
||||
```
|
||||
|
||||
3. Restart Charon — plugins load automatically at startup.
|
||||
|
||||
4. Verify the plugin appears in `GET /api/v1/dns-providers/types` with `is_built_in: false`.
|
||||
|
||||
For detailed plugin installation and security guidance, see [Custom Plugins](../features/custom-plugins.md).
|
||||
|
||||
## General Setup Workflow
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
36
frontend/package-lock.json
generated
36
frontend/package-lock.json
generated
@@ -167,7 +167,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -526,7 +525,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -573,7 +571,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -3284,7 +3281,8 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -3361,7 +3359,6 @@
|
||||
"integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -3372,7 +3369,6 @@
|
||||
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -3383,7 +3379,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -3423,7 +3418,6 @@
|
||||
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.53.0",
|
||||
"@typescript-eslint/types": "8.53.0",
|
||||
@@ -3802,7 +3796,6 @@
|
||||
"integrity": "sha512-hRDjg6dlDz7JlZAvjbiCdAJ3SDG+NH8tjZe21vjxfvT2ssYAn72SRXMge3dKKABm3bIJ3C+3wdunIdur8PHEAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.17",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -3838,7 +3831,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4070,7 +4062,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4277,8 +4268,7 @@
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "6.0.0",
|
||||
@@ -4367,7 +4357,8 @@
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
@@ -4531,7 +4522,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5255,7 +5245,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@@ -5449,7 +5438,6 @@
|
||||
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.28",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
@@ -5896,6 +5884,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -6309,7 +6298,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -6339,6 +6327,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -6353,6 +6342,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -6362,6 +6352,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -6409,7 +6400,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -6419,7 +6409,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -6491,7 +6480,8 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
@@ -7041,7 +7031,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7179,7 +7168,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -7255,7 +7243,6 @@
|
||||
"integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.17",
|
||||
"@vitest/mocker": "4.0.17",
|
||||
@@ -7490,7 +7477,6 @@
|
||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -56,24 +56,29 @@ export interface DNSTestResult {
|
||||
propagation_time_ms?: number
|
||||
}
|
||||
|
||||
/** Field definition for DNS provider credentials */
|
||||
export interface DNSProviderField {
|
||||
name: string
|
||||
label: string
|
||||
type: 'text' | 'password' | 'textarea' | 'select'
|
||||
required: boolean
|
||||
default?: string
|
||||
hint?: string
|
||||
placeholder?: string
|
||||
options?: Array<{
|
||||
value: string
|
||||
label: string
|
||||
}>
|
||||
}
|
||||
|
||||
/** DNS provider type information with field definitions */
|
||||
export interface DNSProviderTypeInfo {
|
||||
type: DNSProviderType
|
||||
name: string
|
||||
fields: Array<{
|
||||
name: string
|
||||
label: string
|
||||
type: 'text' | 'password' | 'textarea' | 'select'
|
||||
required: boolean
|
||||
default?: string
|
||||
hint?: string
|
||||
placeholder?: string
|
||||
options?: Array<{
|
||||
value: string
|
||||
label: string
|
||||
}>
|
||||
}>
|
||||
documentation_url: string
|
||||
description?: string
|
||||
documentation_url?: string
|
||||
is_built_in?: boolean
|
||||
fields: DNSProviderField[]
|
||||
}
|
||||
|
||||
/** Response for list endpoint */
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type DNSProvider,
|
||||
type DNSProviderRequest,
|
||||
type DNSProviderTypeInfo,
|
||||
type DNSProviderField,
|
||||
type DNSTestResult,
|
||||
} from '../api/dnsProviders'
|
||||
|
||||
@@ -111,5 +112,6 @@ export type {
|
||||
DNSProvider,
|
||||
DNSProviderRequest,
|
||||
DNSProviderTypeInfo,
|
||||
DNSProviderField,
|
||||
DNSTestResult,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user