package handlers import ( "auto-ssl/config" "auto-ssl/services" "fmt" "net/http" "path/filepath" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) type CertHandler struct { Cfg *config.Config } func NewCertHandler(cfg *config.Config) *CertHandler { return &CertHandler{Cfg: cfg} } type CreateCertRequest struct { Domain string `json:"domain" binding:"required"` Email string `json:"email" binding:"required"` Provider string `json:"provider"` ChallengeType string `json:"challenge_type"` DNSProvider string `json:"dns_provider"` DNSConfig string `json:"dns_config"` AutoRenew *bool `json:"auto_renew"` RenewDays *int `json:"renew_days"` } // ListCertificates returns all certificates func (h *CertHandler) ListCertificates(c *gin.Context) { certs := config.Store.GetAll() 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 } cert := config.Store.GetByID(uint(id)) if cert == nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } c.JSON(http.StatusOK, cert) } // 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" } req.Domain = strings.TrimSpace(req.Domain) autoRenew := true renewDays := 30 if req.AutoRenew != nil { autoRenew = *req.AutoRenew } if req.RenewDays != nil { renewDays = *req.RenewDays } existing := config.Store.GetByDomain(req.Domain) if existing != nil { // Domain exists: only retry if it's in a retryable state if existing.Status == "error" || existing.Status == "expired" || existing.Status == "pending" { existing.Email = req.Email existing.Provider = req.Provider existing.ChallengeType = req.ChallengeType existing.DNSProvider = req.DNSProvider existing.DNSConfig = req.DNSConfig existing.Status = "pending" existing.ErrorMessage = "" existing.AutoRenew = autoRenew existing.RenewDays = renewDays config.Store.Upsert(existing) // Start issuance in background go func() { domain := existing.Domain saved := config.Store.GetByDomain(domain) if err := services.GetACMECertificate(saved, h.Cfg); err != nil { saved.Status = "error" saved.ErrorMessage = err.Error() } else { saved.Status = "active" } config.Store.Upsert(saved) }() c.JSON(http.StatusAccepted, gin.H{"message": "certificate re-issuance started", "certificate": existing}) return } c.JSON(http.StatusConflict, gin.H{"error": "domain already exists with status: " + existing.Status}) return } // Create new certificate cert := &config.Certificate{ Domain: req.Domain, Email: req.Email, Provider: req.Provider, ChallengeType: req.ChallengeType, DNSProvider: req.DNSProvider, DNSConfig: req.DNSConfig, Status: "pending", AutoRenew: autoRenew, RenewDays: renewDays, } if err := config.Store.Upsert(cert); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Start issuance in background go func() { domain := cert.Domain saved := config.Store.GetByDomain(domain) if err := services.GetACMECertificate(saved, h.Cfg); err != nil { saved.Status = "error" saved.ErrorMessage = err.Error() } else { saved.Status = "active" } config.Store.Upsert(saved) }() c.JSON(http.StatusAccepted, gin.H{"message": "certificate issuance started", "certificate": 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 } cert := config.Store.GetByID(uint(id)) if cert == nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } cert.Status = "renewing" config.Store.Upsert(cert) go func() { domain := cert.Domain saved := config.Store.GetByDomain(domain) if err := services.RenewCertificate(saved, h.Cfg); err != nil { saved.Status = "error" saved.ErrorMessage = err.Error() } else { saved.Status = "active" } config.Store.Upsert(saved) }() 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 } if err := config.Store.Delete(uint(id)); 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 } cert := config.Store.GetByID(uint(id)) if cert == 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 } if v, ok := updates["auto_renew"]; ok { cert.AutoRenew, _ = v.(bool) } if v, ok := updates["renew_days"]; ok { if f, ok := v.(float64); ok { cert.RenewDays = int(f) } } if v, ok := updates["dns_config"]; ok { cert.DNSConfig, _ = v.(string) } config.Store.Upsert(cert) 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 } cert := config.Store.GetByID(uint(id)) if cert == 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) { certs := config.Store.GetActiveWithAutoRenew() 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.Store.Upsert(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) { certs := config.Store.GetAll() var total, active, expired, errors int for _, cert := range certs { total++ switch cert.Status { case "active": active++ case "expired": expired++ case "error": 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 }