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

215 lines
5.9 KiB
Go

package handlers
import (
"auto-ssl/config"
"encoding/json"
"net/http"
"strconv"
"strings"
"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
}
// Trim whitespace from all values
for k, v := range testData {
testData[k] = strings.TrimSpace(v)
}
// 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
}
}
}
// Cloudflare: require either API Token or Email+Key
if req.Type == config.CredentialCloudflare {
hasToken := testData["cf_api_token"] != ""
hasEmailKey := testData["cf_email"] != "" && testData["cf_api_key"] != ""
if !hasToken && !hasEmailKey {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写 API Token,或者同时填写 Email 和 Global API Key"})
return
}
}
// Re-serialize trimmed data
trimmedData, _ := json.Marshal(testData)
cred := &config.Credential{
Name: req.Name,
Type: req.Type,
Data: string(trimmedData),
}
if err := config.CredStore.Create(cred); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Return without secrets (use a copy to avoid mutating store)
resp := *cred
resp.Data = cred.Masked
c.JSON(http.StatusCreated, &resp)
}
// 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 unchanged
if req.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"})
return
}
// Trim whitespace from all values
for k, v := range testData {
testData[k] = strings.TrimSpace(v)
}
// Cloudflare: require either API Token or Email+Key
if existing.Type == config.CredentialCloudflare {
hasToken := testData["cf_api_token"] != ""
hasEmailKey := testData["cf_email"] != "" && testData["cf_api_key"] != ""
if !hasToken && !hasEmailKey {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写 API Token,或者同时填写 Email 和 Global API Key"})
return
}
}
trimmedData, _ := json.Marshal(testData)
existing.Data = string(trimmedData)
}
}
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)
}