feat: add DNS credential management with Web UI
- New config/credential_store.go: persistent credential storage (credentials.json) - New API: CRUD for DNS credentials + credential-types endpoint - Certificate model now supports credential_id reference - Create certificate auto-resolves credential_id to DNS config - New Vue page: Credentials.vue for managing saved DNS keys - CertCreate.vue: select existing credential or manual input + save as new - Secrets masked in API responses, never exposed in list
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"auto-ssl/config"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CredentialHandler struct{}
|
||||
|
||||
func NewCredentialHandler() *CredentialHandler {
|
||||
return &CredentialHandler{}
|
||||
}
|
||||
|
||||
type CreateCredentialRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type config.CredentialType `json:"type" binding:"required"`
|
||||
Data string `json:"data" binding:"required"` // JSON key-value pairs
|
||||
}
|
||||
|
||||
type UpdateCredentialRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type config.CredentialType `json:"type"`
|
||||
Data string `json:"data"` // JSON key-value pairs (or "********" to keep)
|
||||
}
|
||||
|
||||
// ListCredentials returns all credentials (without secrets)
|
||||
func (h *CredentialHandler) ListCredentials(c *gin.Context) {
|
||||
creds := config.CredStore.GetAll()
|
||||
c.JSON(http.StatusOK, creds)
|
||||
}
|
||||
|
||||
// GetCredential returns a single credential
|
||||
func (h *CredentialHandler) GetCredential(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
cred := config.CredStore.GetByID(uint(id))
|
||||
if cred == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "credential not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Never expose raw secrets in API
|
||||
cred.Data = cred.Masked
|
||||
c.JSON(http.StatusOK, cred)
|
||||
}
|
||||
|
||||
// CreateCredential creates a new credential
|
||||
func (h *CredentialHandler) CreateCredential(c *gin.Context) {
|
||||
var req CreateCredentialRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate JSON data
|
||||
var testData map[string]string
|
||||
if err := json.Unmarshal([]byte(req.Data), &testData); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "data must be valid JSON key-value pairs"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate fields for the provider type
|
||||
fields := config.CredentialTypeMeta(req.Type)
|
||||
for _, f := range fields {
|
||||
if f.Required {
|
||||
if _, ok := testData[f.Key]; !ok || testData[f.Key] == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": f.Label + " 是必填项"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cred := &config.Credential{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Data: req.Data,
|
||||
}
|
||||
|
||||
if err := config.CredStore.Create(cred); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Return without secrets
|
||||
cred.Data = cred.Masked
|
||||
c.JSON(http.StatusCreated, cred)
|
||||
}
|
||||
|
||||
// UpdateCredential updates a credential
|
||||
func (h *CredentialHandler) UpdateCredential(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
existing := config.CredStore.GetByID(uint(id))
|
||||
if existing == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "credential not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateCredentialRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
existing.Name = req.Name
|
||||
}
|
||||
if req.Type != "" {
|
||||
existing.Type = req.Type
|
||||
}
|
||||
if req.Data != "" {
|
||||
// If it's the masked placeholder, keep existing data
|
||||
if req.Data == "********" {
|
||||
existing.Data = existing.Data
|
||||
} else {
|
||||
var testData map[string]string
|
||||
if err := json.Unmarshal([]byte(req.Data), &testData); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "data must be valid JSON"})
|
||||
return
|
||||
}
|
||||
existing.Data = req.Data
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.CredStore.Update(existing); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
existing.Data = existing.Masked
|
||||
c.JSON(http.StatusOK, existing)
|
||||
}
|
||||
|
||||
// DeleteCredential deletes a credential
|
||||
func (h *CredentialHandler) DeleteCredential(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := config.CredStore.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "credential deleted"})
|
||||
}
|
||||
|
||||
// GetCredentialTypes returns available credential types and their fields
|
||||
func (h *CredentialHandler) GetCredentialTypes(c *gin.Context) {
|
||||
types := []gin.H{
|
||||
{
|
||||
"type": config.CredentialAliDNS,
|
||||
"label": "阿里云 DNS (Aliyun)",
|
||||
"fields": config.CredentialTypeMeta(config.CredentialAliDNS),
|
||||
},
|
||||
{
|
||||
"type": config.CredentialCloudflare,
|
||||
"label": "Cloudflare",
|
||||
"fields": config.CredentialTypeMeta(config.CredentialCloudflare),
|
||||
},
|
||||
{
|
||||
"type": config.CredentialDNSPod,
|
||||
"label": "DNSPod",
|
||||
"fields": config.CredentialTypeMeta(config.CredentialDNSPod),
|
||||
},
|
||||
}
|
||||
c.JSON(http.StatusOK, types)
|
||||
}
|
||||
Reference in New Issue
Block a user