Files
Charon/backend/internal/api/handlers/dns_provider_handler.go
GitHub Actions 77a020b4db 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
2026-01-14 18:05:46 +00:00

298 lines
8.2 KiB
Go

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"
)
// DNSProviderHandler handles DNS provider API requests.
type DNSProviderHandler struct {
service services.DNSProviderService
}
// NewDNSProviderHandler creates a new DNS provider handler.
func NewDNSProviderHandler(service services.DNSProviderService) *DNSProviderHandler {
return &DNSProviderHandler{
service: service,
}
}
// List handles GET /api/v1/dns-providers
// Returns all DNS providers without exposing credentials.
func (h *DNSProviderHandler) List(c *gin.Context) {
providers, err := h.service.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list DNS providers"})
return
}
// Convert to response format with has_credentials indicator
responses := make([]services.DNSProviderResponse, len(providers))
for i, p := range providers {
responses[i] = services.DNSProviderResponse{
DNSProvider: p,
HasCredentials: p.CredentialsEncrypted != "",
}
}
c.JSON(http.StatusOK, gin.H{
"providers": responses,
"total": len(responses),
})
}
// Get handles GET /api/v1/dns-providers/:id
// Returns a single DNS provider without exposing credentials.
func (h *DNSProviderHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
return
}
provider, err := h.service.Get(c.Request.Context(), uint(id))
if err != nil {
if err == services.ErrDNSProviderNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "DNS provider not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve DNS provider"})
return
}
response := services.DNSProviderResponse{
DNSProvider: *provider,
HasCredentials: provider.CredentialsEncrypted != "",
}
c.JSON(http.StatusOK, response)
}
// Create handles POST /api/v1/dns-providers
// Creates a new DNS provider with encrypted credentials.
func (h *DNSProviderHandler) Create(c *gin.Context) {
var req services.CreateDNSProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
provider, err := h.service.Create(c.Request.Context(), req)
if err != nil {
statusCode := http.StatusBadRequest
errorMessage := err.Error()
switch err {
case services.ErrInvalidProviderType:
errorMessage = "Unsupported DNS provider type"
case services.ErrInvalidCredentials:
errorMessage = "Invalid credentials: missing required fields"
case services.ErrEncryptionFailed:
statusCode = http.StatusInternalServerError
errorMessage = "Failed to encrypt credentials"
}
c.JSON(statusCode, gin.H{"error": errorMessage})
return
}
response := services.DNSProviderResponse{
DNSProvider: *provider,
HasCredentials: provider.CredentialsEncrypted != "",
}
c.JSON(http.StatusCreated, response)
}
// Update handles PUT /api/v1/dns-providers/:id
// Updates an existing DNS provider.
func (h *DNSProviderHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
return
}
var req services.UpdateDNSProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
provider, err := h.service.Update(c.Request.Context(), uint(id), req)
if err != nil {
statusCode := http.StatusBadRequest
errorMessage := err.Error()
switch err {
case services.ErrDNSProviderNotFound:
statusCode = http.StatusNotFound
errorMessage = "DNS provider not found"
case services.ErrInvalidCredentials:
errorMessage = "Invalid credentials: missing required fields"
case services.ErrEncryptionFailed:
statusCode = http.StatusInternalServerError
errorMessage = "Failed to encrypt credentials"
}
c.JSON(statusCode, gin.H{"error": errorMessage})
return
}
response := services.DNSProviderResponse{
DNSProvider: *provider,
HasCredentials: provider.CredentialsEncrypted != "",
}
c.JSON(http.StatusOK, response)
}
// Delete handles DELETE /api/v1/dns-providers/:id
// Deletes a DNS provider.
func (h *DNSProviderHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
return
}
err = h.service.Delete(c.Request.Context(), uint(id))
if err != nil {
if err == services.ErrDNSProviderNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "DNS provider not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete DNS provider"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "DNS provider deleted successfully"})
}
// Test handles POST /api/v1/dns-providers/:id/test
// Tests a saved DNS provider's credentials.
func (h *DNSProviderHandler) Test(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
return
}
result, err := h.service.Test(c.Request.Context(), uint(id))
if err != nil {
if err == services.ErrDNSProviderNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "DNS provider not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to test DNS provider"})
return
}
c.JSON(http.StatusOK, result)
}
// TestCredentials handles POST /api/v1/dns-providers/test
// Tests DNS provider credentials without saving them.
func (h *DNSProviderHandler) TestCredentials(c *gin.Context) {
var req services.CreateDNSProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := h.service.TestCredentials(c.Request.Context(), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to test credentials"})
return
}
c.JSON(http.StatusOK, result)
}
// 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) {
// 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,
})
}