cert.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. package handlers
  2. import (
  3. "auto-ssl/config"
  4. "auto-ssl/services"
  5. "fmt"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/gin-gonic/gin"
  11. )
  12. type CertHandler struct {
  13. Cfg *config.Config
  14. }
  15. func NewCertHandler(cfg *config.Config) *CertHandler {
  16. return &CertHandler{Cfg: cfg}
  17. }
  18. type CreateCertRequest struct {
  19. Domain string `json:"domain" binding:"required"`
  20. Email string `json:"email" binding:"required"`
  21. Provider string `json:"provider"`
  22. ChallengeType string `json:"challenge_type"`
  23. DNSProvider string `json:"dns_provider"`
  24. DNSConfig string `json:"dns_config"`
  25. AutoRenew *bool `json:"auto_renew"`
  26. RenewDays *int `json:"renew_days"`
  27. }
  28. // ListCertificates returns all certificates
  29. func (h *CertHandler) ListCertificates(c *gin.Context) {
  30. certs := config.Store.GetAll()
  31. c.JSON(http.StatusOK, certs)
  32. }
  33. // GetCertificate returns a single certificate
  34. func (h *CertHandler) GetCertificate(c *gin.Context) {
  35. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  36. if err != nil {
  37. c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
  38. return
  39. }
  40. cert := config.Store.GetByID(uint(id))
  41. if cert == nil {
  42. c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
  43. return
  44. }
  45. c.JSON(http.StatusOK, cert)
  46. }
  47. // CreateCertificate creates a new certificate entry and starts issuance
  48. func (h *CertHandler) CreateCertificate(c *gin.Context) {
  49. var req CreateCertRequest
  50. if err := c.ShouldBindJSON(&req); err != nil {
  51. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  52. return
  53. }
  54. if req.Provider == "" {
  55. req.Provider = "letsencrypt"
  56. }
  57. if req.ChallengeType == "" {
  58. req.ChallengeType = "http"
  59. }
  60. req.Domain = strings.TrimSpace(req.Domain)
  61. autoRenew := true
  62. renewDays := 30
  63. if req.AutoRenew != nil {
  64. autoRenew = *req.AutoRenew
  65. }
  66. if req.RenewDays != nil {
  67. renewDays = *req.RenewDays
  68. }
  69. existing := config.Store.GetByDomain(req.Domain)
  70. if existing != nil {
  71. // Domain exists: only retry if it's in a retryable state
  72. if existing.Status == "error" || existing.Status == "expired" || existing.Status == "pending" {
  73. existing.Email = req.Email
  74. existing.Provider = req.Provider
  75. existing.ChallengeType = req.ChallengeType
  76. existing.DNSProvider = req.DNSProvider
  77. existing.DNSConfig = req.DNSConfig
  78. existing.Status = "pending"
  79. existing.ErrorMessage = ""
  80. existing.AutoRenew = autoRenew
  81. existing.RenewDays = renewDays
  82. config.Store.Upsert(existing)
  83. // Start issuance in background
  84. go func() {
  85. domain := existing.Domain
  86. saved := config.Store.GetByDomain(domain)
  87. if err := services.GetACMECertificate(saved, h.Cfg); err != nil {
  88. saved.Status = "error"
  89. saved.ErrorMessage = err.Error()
  90. } else {
  91. saved.Status = "active"
  92. }
  93. config.Store.Upsert(saved)
  94. }()
  95. c.JSON(http.StatusAccepted, gin.H{"message": "certificate re-issuance started", "certificate": existing})
  96. return
  97. }
  98. c.JSON(http.StatusConflict, gin.H{"error": "domain already exists with status: " + existing.Status})
  99. return
  100. }
  101. // Create new certificate
  102. cert := &config.Certificate{
  103. Domain: req.Domain,
  104. Email: req.Email,
  105. Provider: req.Provider,
  106. ChallengeType: req.ChallengeType,
  107. DNSProvider: req.DNSProvider,
  108. DNSConfig: req.DNSConfig,
  109. Status: "pending",
  110. AutoRenew: autoRenew,
  111. RenewDays: renewDays,
  112. }
  113. if err := config.Store.Upsert(cert); err != nil {
  114. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  115. return
  116. }
  117. // Start issuance in background
  118. go func() {
  119. domain := cert.Domain
  120. saved := config.Store.GetByDomain(domain)
  121. if err := services.GetACMECertificate(saved, h.Cfg); err != nil {
  122. saved.Status = "error"
  123. saved.ErrorMessage = err.Error()
  124. } else {
  125. saved.Status = "active"
  126. }
  127. config.Store.Upsert(saved)
  128. }()
  129. c.JSON(http.StatusAccepted, gin.H{"message": "certificate issuance started", "certificate": cert})
  130. }
  131. // RenewCertificate manually renews a certificate
  132. func (h *CertHandler) RenewCertificate(c *gin.Context) {
  133. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  134. if err != nil {
  135. c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
  136. return
  137. }
  138. cert := config.Store.GetByID(uint(id))
  139. if cert == nil {
  140. c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
  141. return
  142. }
  143. cert.Status = "renewing"
  144. config.Store.Upsert(cert)
  145. go func() {
  146. domain := cert.Domain
  147. saved := config.Store.GetByDomain(domain)
  148. if err := services.RenewCertificate(saved, h.Cfg); err != nil {
  149. saved.Status = "error"
  150. saved.ErrorMessage = err.Error()
  151. } else {
  152. saved.Status = "active"
  153. }
  154. config.Store.Upsert(saved)
  155. }()
  156. c.JSON(http.StatusAccepted, gin.H{"message": "renewal started", "certificate": cert})
  157. }
  158. // DeleteCertificate deletes a certificate record and files
  159. func (h *CertHandler) DeleteCertificate(c *gin.Context) {
  160. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  161. if err != nil {
  162. c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
  163. return
  164. }
  165. if err := config.Store.Delete(uint(id)); err != nil {
  166. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  167. return
  168. }
  169. c.JSON(http.StatusOK, gin.H{"message": "certificate deleted"})
  170. }
  171. // UpdateCertificate updates certificate settings
  172. func (h *CertHandler) UpdateCertificate(c *gin.Context) {
  173. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  174. if err != nil {
  175. c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
  176. return
  177. }
  178. cert := config.Store.GetByID(uint(id))
  179. if cert == nil {
  180. c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
  181. return
  182. }
  183. var updates map[string]interface{}
  184. if err := c.ShouldBindJSON(&updates); err != nil {
  185. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  186. return
  187. }
  188. if v, ok := updates["auto_renew"]; ok {
  189. cert.AutoRenew, _ = v.(bool)
  190. }
  191. if v, ok := updates["renew_days"]; ok {
  192. if f, ok := v.(float64); ok {
  193. cert.RenewDays = int(f)
  194. }
  195. }
  196. if v, ok := updates["dns_config"]; ok {
  197. cert.DNSConfig, _ = v.(string)
  198. }
  199. config.Store.Upsert(cert)
  200. c.JSON(http.StatusOK, cert)
  201. }
  202. // GetCertFiles returns the content of certificate files for download
  203. func (h *CertHandler) GetCertFiles(c *gin.Context) {
  204. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  205. if err != nil {
  206. c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
  207. return
  208. }
  209. cert := config.Store.GetByID(uint(id))
  210. if cert == nil {
  211. c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
  212. return
  213. }
  214. fullchain, privkey, chain := services.GetCertFileContents(cert.Domain, h.Cfg)
  215. result := gin.H{
  216. "domain": cert.Domain,
  217. "fullchain": fullchain,
  218. "privkey": privkey,
  219. "chain": chain,
  220. }
  221. c.JSON(http.StatusOK, result)
  222. }
  223. // CheckRenewals checks all certificates and renews those about to expire
  224. func (h *CertHandler) CheckRenewals(c *gin.Context) {
  225. certs := config.Store.GetActiveWithAutoRenew()
  226. renewed := []string{}
  227. failed := []string{}
  228. for _, cert := range certs {
  229. if cert.ExpiresAt != nil && time.Until(*cert.ExpiresAt).Hours() < float64(cert.RenewDays*24) {
  230. if err := services.RenewCertificate(cert, h.Cfg); err != nil {
  231. cert.Status = "error"
  232. cert.ErrorMessage = fmt.Sprintf("auto renew failed: %v", err)
  233. failed = append(failed, cert.Domain)
  234. } else {
  235. cert.Status = "active"
  236. renewed = append(renewed, cert.Domain)
  237. }
  238. config.Store.Upsert(cert)
  239. }
  240. }
  241. c.JSON(http.StatusOK, gin.H{
  242. "message": "renewal check complete",
  243. "renewed": renewed,
  244. "failed": failed,
  245. })
  246. }
  247. // Stats returns dashboard statistics
  248. func (h *CertHandler) Stats(c *gin.Context) {
  249. certs := config.Store.GetAll()
  250. var total, active, expired, errors int
  251. for _, cert := range certs {
  252. total++
  253. switch cert.Status {
  254. case "active":
  255. active++
  256. case "expired":
  257. expired++
  258. case "error":
  259. errors++
  260. }
  261. }
  262. c.JSON(http.StatusOK, gin.H{
  263. "total": total,
  264. "active": active,
  265. "expired": expired,
  266. "errors": errors,
  267. })
  268. }