feat: AutoSSL certificate management tool with Web UI
Cette révision appartient à :
@@ -0,0 +1,293 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"auto-ssl/config"
|
||||
"auto-ssl/services"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CertHandler struct {
|
||||
Cfg *config.Config
|
||||
}
|
||||
|
||||
func NewCertHandler(cfg *config.Config) *CertHandler {
|
||||
return &CertHandler{Cfg: cfg}
|
||||
}
|
||||
|
||||
// ListCertificates returns all certificates
|
||||
func (h *CertHandler) ListCertificates(c *gin.Context) {
|
||||
var certs []config.Certificate
|
||||
if err := config.DB.Order("created_at desc").Find(&certs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, certs)
|
||||
}
|
||||
|
||||
// GetCertificate returns a single certificate
|
||||
func (h *CertHandler) GetCertificate(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
|
||||
}
|
||||
var cert config.Certificate
|
||||
if err := config.DB.First(&cert, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, cert)
|
||||
}
|
||||
|
||||
type CreateCertRequest struct {
|
||||
Domain string `json:"domain" binding:"required"`
|
||||
Email string `json:"email" binding:"required"`
|
||||
Provider string `json:"provider"` // letsencrypt, zerossl
|
||||
ChallengeType string `json:"challenge_type"` // http, dns
|
||||
DNSProvider string `json:"dns_provider"`
|
||||
DNSConfig string `json:"dns_config"` // JSON
|
||||
AutoRenew *bool `json:"auto_renew"`
|
||||
RenewDays *int `json:"renew_days"`
|
||||
}
|
||||
|
||||
// CreateCertificate creates a new certificate entry and starts issuance
|
||||
func (h *CertHandler) CreateCertificate(c *gin.Context) {
|
||||
var req CreateCertRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Provider == "" {
|
||||
req.Provider = "letsencrypt"
|
||||
}
|
||||
if req.ChallengeType == "" {
|
||||
req.ChallengeType = "http"
|
||||
}
|
||||
|
||||
// Trim spaces from domain
|
||||
req.Domain = strings.TrimSpace(req.Domain)
|
||||
|
||||
// Check if domain already exists
|
||||
var existing config.Certificate
|
||||
if err := config.DB.Where("domain = ?", req.Domain).First(&existing).Error; err == nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "domain already exists"})
|
||||
return
|
||||
}
|
||||
|
||||
cert := config.Certificate{
|
||||
Domain: req.Domain,
|
||||
Email: req.Email,
|
||||
Provider: req.Provider,
|
||||
ChallengeType: req.ChallengeType,
|
||||
DNSProvider: req.DNSProvider,
|
||||
DNSConfig: req.DNSConfig,
|
||||
Status: "pending",
|
||||
AutoRenew: true,
|
||||
RenewDays: 30,
|
||||
}
|
||||
|
||||
if req.AutoRenew != nil {
|
||||
cert.AutoRenew = *req.AutoRenew
|
||||
}
|
||||
if req.RenewDays != nil {
|
||||
cert.RenewDays = *req.RenewDays
|
||||
}
|
||||
|
||||
if err := config.DB.Create(&cert).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Start issuance in background
|
||||
go func() {
|
||||
if err := services.GetACMECertificate(&cert, h.Cfg); err != nil {
|
||||
cert.Status = "error"
|
||||
cert.ErrorMessage = err.Error()
|
||||
} else {
|
||||
cert.Status = "active"
|
||||
}
|
||||
config.DB.Save(&cert)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusAccepted, cert)
|
||||
}
|
||||
|
||||
// RenewCertificate manually renews a certificate
|
||||
func (h *CertHandler) RenewCertificate(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
|
||||
}
|
||||
|
||||
var cert config.Certificate
|
||||
if err := config.DB.First(&cert, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||
return
|
||||
}
|
||||
|
||||
cert.Status = "renewing"
|
||||
config.DB.Save(&cert)
|
||||
|
||||
go func() {
|
||||
if err := services.RenewCertificate(&cert, h.Cfg); err != nil {
|
||||
cert.Status = "error"
|
||||
cert.ErrorMessage = err.Error()
|
||||
} else {
|
||||
cert.Status = "active"
|
||||
}
|
||||
config.DB.Save(&cert)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{"message": "renewal started", "certificate": cert})
|
||||
}
|
||||
|
||||
// DeleteCertificate deletes a certificate record and files
|
||||
func (h *CertHandler) DeleteCertificate(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
|
||||
}
|
||||
|
||||
var cert config.Certificate
|
||||
if err := config.DB.First(&cert, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := config.DB.Delete(&cert).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "certificate deleted"})
|
||||
}
|
||||
|
||||
// UpdateCertificate updates certificate settings
|
||||
func (h *CertHandler) UpdateCertificate(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
|
||||
}
|
||||
|
||||
var cert config.Certificate
|
||||
if err := config.DB.First(&cert, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Only allow updating certain fields
|
||||
allowedFields := map[string]bool{
|
||||
"auto_renew": true,
|
||||
"renew_days": true,
|
||||
"dns_config": true,
|
||||
}
|
||||
|
||||
filtered := make(map[string]interface{})
|
||||
for k, v := range updates {
|
||||
if allowedFields[k] {
|
||||
filtered[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.DB.Model(&cert).Updates(filtered).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, cert)
|
||||
}
|
||||
|
||||
// GetCertFiles returns the content of certificate files for download
|
||||
func (h *CertHandler) GetCertFiles(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
|
||||
}
|
||||
|
||||
var cert config.Certificate
|
||||
if err := config.DB.First(&cert, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||
return
|
||||
}
|
||||
|
||||
fullchain, privkey, chain := services.GetCertFilesPaths(cert.Domain, h.Cfg)
|
||||
|
||||
result := gin.H{
|
||||
"domain": cert.Domain,
|
||||
"fullchain": readFileSafe(fullchain),
|
||||
"privkey": readFileSafe(privkey),
|
||||
"chain": readFileSafe(chain),
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// CheckRenewals checks all certificates and renews those about to expire
|
||||
func (h *CertHandler) CheckRenewals(c *gin.Context) {
|
||||
var certs []config.Certificate
|
||||
config.DB.Where("auto_renew = ? AND status = ?", true, "active").Find(&certs)
|
||||
|
||||
renewed := []string{}
|
||||
failed := []string{}
|
||||
|
||||
for _, cert := range certs {
|
||||
if cert.ExpiresAt != nil && time.Until(*cert.ExpiresAt).Hours() < float64(cert.RenewDays*24) {
|
||||
if err := services.RenewCertificate(&cert, h.Cfg); err != nil {
|
||||
cert.Status = "error"
|
||||
cert.ErrorMessage = fmt.Sprintf("auto renew failed: %v", err)
|
||||
failed = append(failed, cert.Domain)
|
||||
} else {
|
||||
cert.Status = "active"
|
||||
renewed = append(renewed, cert.Domain)
|
||||
}
|
||||
config.DB.Save(&cert)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "renewal check complete",
|
||||
"renewed": renewed,
|
||||
"failed": failed,
|
||||
})
|
||||
}
|
||||
|
||||
// Stats returns dashboard statistics
|
||||
func (h *CertHandler) Stats(c *gin.Context) {
|
||||
var total, active, expired, errors int64
|
||||
config.DB.Model(&config.Certificate{}).Count(&total)
|
||||
config.DB.Model(&config.Certificate{}).Where("status = ?", "active").Count(&active)
|
||||
config.DB.Model(&config.Certificate{}).Where("status = ?", "expired").Count(&expired)
|
||||
config.DB.Model(&config.Certificate{}).Where("status = ?", "error").Count(&errors)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"total": total,
|
||||
"active": active,
|
||||
"expired": expired,
|
||||
"errors": errors,
|
||||
})
|
||||
}
|
||||
|
||||
func readFileSafe(path string) string {
|
||||
data, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return data
|
||||
}
|
||||
Référencer dans un nouveau ticket
Bloquer un utilisateur