From 3fa77d9bc05d7de0cb45ad97b510712b0a2d31ce Mon Sep 17 00:00:00 2001 From: cnbugs <717192502@qq.com> Date: Tue, 12 May 2026 15:27:10 +0800 Subject: [PATCH] refactor: replace SQLite/GORM with JSON file storage - Remove GORM and SQLite, use simple JSON file (data/certs.json) for persistence - CertStore with sync.RWMutex for thread-safe in-memory cache - All handlers updated to use config.Store instead of config.DB - Cron job and stats updated accordingly - go mod tidy removes unused gorm/sqlite dependencies --- backend/config/certificate.go | 38 +++--- backend/config/config.go | 179 ++++++++++++++++++++++++---- backend/go.mod | 5 - backend/go.sum | 10 -- backend/handlers/cert.go | 213 ++++++++++++++++++---------------- backend/main.go | 11 +- 6 files changed, 292 insertions(+), 164 deletions(-) diff --git a/backend/config/certificate.go b/backend/config/certificate.go index 21a7ad4..563c6af 100644 --- a/backend/config/certificate.go +++ b/backend/config/certificate.go @@ -1,33 +1,25 @@ package config -import ( - "time" - "gorm.io/gorm" -) +import "time" type Certificate struct { - ID uint `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - Domain string `json:"domain" gorm:"uniqueIndex;size:255"` - Email string `json:"email" gorm:"size:255"` - Provider string `json:"provider" gorm:"size:50;default:letsencrypt"` // letsencrypt, zerossl - ChallengeType string `json:"challenge_type" gorm:"size:20;default:http"` // http, dns - DNSProvider string `json:"dns_provider,omitempty" gorm:"size:50"` // alidns, cloudflare, etc. - DNSConfig string `json:"dns_config,omitempty" gorm:"type:text"` // JSON config for DNS provider + Domain string `json:"domain"` + Email string `json:"email"` + Provider string `json:"provider"` // letsencrypt, zerossl + ChallengeType string `json:"challenge_type"` // http, dns + DNSProvider string `json:"dns_provider,omitempty"` // alidns, cloudflare, etc. + DNSConfig string `json:"dns_config,omitempty"` // JSON config for DNS provider - Status string `json:"status" gorm:"size:20;default:pending"` // pending, active, expired, error - CertURL string `json:"cert_url,omitempty" gorm:"size:512"` + Status string `json:"status"` // pending, active, expired, error + CertURL string `json:"cert_url,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` LastRenewedAt *time.Time `json:"last_renewed_at,omitempty"` - ErrorMessage string `json:"error_message,omitempty" gorm:"type:text"` + ErrorMessage string `json:"error_message,omitempty"` - // Auto renew settings - AutoRenew bool `json:"auto_renew" gorm:"default:true"` - RenewDays int `json:"renew_days" gorm:"default:30"` // Renew when expires within this many days - - // ACME account key - AccountKeyID uint `json:"account_key_id,omitempty"` + AutoRenew bool `json:"auto_renew"` + RenewDays int `json:"renew_days"` // Renew when expires within this many days } diff --git a/backend/config/config.go b/backend/config/config.go index aaf0afb..4176cec 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -1,20 +1,18 @@ package config import ( - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" + "encoding/json" "log" "os" + "sync" + "time" ) -var DB *gorm.DB - type Config struct { Port string - DBPath string CertDir string AccountsDir string + DataDir string } func Load() *Config { @@ -22,10 +20,6 @@ func Load() *Config { if port == "" { port = "8080" } - dbPath := os.Getenv("DB_PATH") - if dbPath == "" { - dbPath = "./data/autossl.db" - } certDir := os.Getenv("CERT_DIR") if certDir == "" { certDir = "./data/certs" @@ -34,36 +28,175 @@ func Load() *Config { if accountsDir == "" { accountsDir = "./data/accounts" } + dataDir := os.Getenv("DATA_DIR") + if dataDir == "" { + dataDir = "./data" + } return &Config{ Port: port, - DBPath: dbPath, CertDir: certDir, AccountsDir: accountsDir, + DataDir: dataDir, } } -func InitDB(cfg *Config) { +// CertStore is the in-memory store for certificates with file persistence +type CertStore struct { + mu sync.RWMutex + data map[string]*Certificate // key: domain + path string + nextID uint +} + +var Store *CertStore + +func InitStore(cfg *Config) { // Ensure data directories exist - dirs := []string{"./data", cfg.CertDir, cfg.AccountsDir} + dirs := []string{cfg.DataDir, cfg.CertDir, cfg.AccountsDir} for _, d := range dirs { if err := os.MkdirAll(d, 0700); err != nil { log.Fatalf("Failed to create directory %s: %v", d, err) } } - var err error - DB, err = gorm.Open(sqlite.Open(cfg.DBPath), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Warn), - }) - if err != nil { - log.Fatalf("Failed to connect database: %v", err) + Store = &CertStore{ + data: make(map[string]*Certificate), + path: cfg.DataDir + "/certs.json", } - // Auto migrate - if err := DB.AutoMigrate(&Certificate{}); err != nil { - log.Fatalf("Failed to migrate database: %v", err) + if err := Store.Load(); err != nil { + log.Printf("No existing cert store or failed to load: %v, starting fresh", err) } - log.Println("Database initialized successfully") + log.Println("Cert store initialized successfully") +} + +// Load reads certificates from JSON file into memory +func (s *CertStore) Load() error { + s.mu.Lock() + defer s.mu.Unlock() + + data, err := os.ReadFile(s.path) + if err != nil { + return err + } + + var certs []*Certificate + if err := json.Unmarshal(data, &certs); err != nil { + return err + } + + s.data = make(map[string]*Certificate) + s.nextID = 0 + for _, c := range certs { + s.data[c.Domain] = c + if c.ID >= s.nextID { + s.nextID = c.ID + 1 + } + } + + return nil +} + +// Save writes the current in-memory certificates to JSON file +func (s *CertStore) Save() error { + s.mu.RLock() + defer s.mu.RUnlock() + + certs := make([]*Certificate, 0, len(s.data)) + for _, c := range s.data { + certs = append(certs, c) + } + + data, err := json.MarshalIndent(certs, "", " ") + if err != nil { + return err + } + + return os.WriteFile(s.path, data, 0600) +} + +// GetAll returns all certificates sorted by ID descending +func (s *CertStore) GetAll() []*Certificate { + s.mu.RLock() + defer s.mu.RUnlock() + + certs := make([]*Certificate, 0, len(s.data)) + for _, c := range s.data { + certs = append(certs, c) + } + + // Sort by ID descending (newest first) + for i := 0; i < len(certs)-1; i++ { + for j := i + 1; j < len(certs); j++ { + if certs[i].ID < certs[j].ID { + certs[i], certs[j] = certs[j], certs[i] + } + } + } + return certs +} + +// GetByDomain returns a certificate by domain +func (s *CertStore) GetByDomain(domain string) *Certificate { + s.mu.RLock() + defer s.mu.RUnlock() + return s.data[domain] +} + +// GetByID returns a certificate by ID +func (s *CertStore) GetByID(id uint) *Certificate { + s.mu.RLock() + defer s.mu.RUnlock() + for _, c := range s.data { + if c.ID == id { + return c + } + } + return nil +} + +// Upsert inserts or updates a certificate +func (s *CertStore) Upsert(cert *Certificate) error { + s.mu.Lock() + defer s.mu.Unlock() + + if cert.ID == 0 { + cert.ID = s.nextID + s.nextID++ + cert.CreatedAt = time.Now() + } + cert.UpdatedAt = time.Now() + + s.data[cert.Domain] = cert + return s.Save() +} + +// Delete removes a certificate by ID +func (s *CertStore) Delete(id uint) error { + s.mu.Lock() + defer s.mu.Unlock() + + for domain, c := range s.data { + if c.ID == id { + delete(s.data, domain) + return s.Save() + } + } + return nil +} + +// GetActiveWithAutoRenew returns all active certs with auto-renewal enabled +func (s *CertStore) GetActiveWithAutoRenew() []*Certificate { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*Certificate + for _, c := range s.data { + if c.AutoRenew && c.Status == "active" { + result = append(result, c) + } + } + return result } diff --git a/backend/go.mod b/backend/go.mod index 52b074c..98c528e 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,8 +7,6 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/go-acme/lego/v4 v4.14.2 github.com/robfig/cron/v3 v3.0.1 - gorm.io/driver/sqlite v1.5.4 - gorm.io/gorm v1.25.5 ) require ( @@ -28,14 +26,11 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.4 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/miekg/dns v1.1.55 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/backend/go.sum b/backend/go.sum index 6171cb0..4519840 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -58,10 +58,6 @@ github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXc github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -86,8 +82,6 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -168,9 +162,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= -gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= -gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= -gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/handlers/cert.go b/backend/handlers/cert.go index e734b4f..756613c 100644 --- a/backend/handlers/cert.go +++ b/backend/handlers/cert.go @@ -7,10 +7,10 @@ import ( "net/http" "path/filepath" "strconv" + "strings" "time" "github.com/gin-gonic/gin" - "strings" ) type CertHandler struct { @@ -21,13 +21,20 @@ 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) { - 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 - } + certs := config.Store.GetAll() c.JSON(http.StatusOK, certs) } @@ -38,25 +45,14 @@ func (h *CertHandler) GetCertificate(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } - var cert config.Certificate - if err := config.DB.First(&cert, id).Error; err != nil { + 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) } -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 @@ -72,56 +68,81 @@ func (h *CertHandler) CreateCertificate(c *gin.Context) { req.ChallengeType = "http" } - // Trim spaces from domain req.Domain = strings.TrimSpace(req.Domain) - // Use SQLite upsert (INSERT ... ON CONFLICT) to atomically handle domain uniqueness - // This avoids the race condition between checking existence and inserting - result := config.DB.Exec(` - INSERT INTO certificates (domain, email, provider, challenge_type, dns_provider, dns_config, status, auto_renew, renew_days, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, datetime('now'), datetime('now')) - ON CONFLICT(domain) DO UPDATE SET - email = excluded.email, - provider = excluded.provider, - challenge_type = excluded.challenge_type, - dns_provider = excluded.dns_provider, - dns_config = excluded.dns_config, - status = CASE - WHEN status IN ('error', 'expired', 'pending') THEN 'pending' - ELSE status - END, - error_message = CASE - WHEN status IN ('error', 'expired', 'pending') THEN '' - ELSE error_message - END, - updated_at = datetime('now') - WHERE excluded.domain = certificates.domain - `, req.Domain, req.Email, req.Provider, req.ChallengeType, req.DNSProvider, req.DNSConfig, req.AutoRenew != nil && *req.AutoRenew, req.RenewDays) + autoRenew := true + renewDays := 30 + if req.AutoRenew != nil { + autoRenew = *req.AutoRenew + } + if req.RenewDays != nil { + renewDays = *req.RenewDays + } - if result.Error != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + 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 } - // Reload to get the current state - var cert config.Certificate - config.DB.Where("domain = ?", req.Domain).First(&cert) + // 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 domain already existed and was active, return conflict - if cert.Status != "pending" { - c.JSON(http.StatusConflict, gin.H{"error": "domain already exists with status: " + cert.Status}) + if err := config.Store.Upsert(cert); 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() + 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 { - cert.Status = "active" + saved.Status = "active" } - config.DB.Save(&cert) + config.Store.Upsert(saved) }() c.JSON(http.StatusAccepted, gin.H{"message": "certificate issuance started", "certificate": cert}) @@ -135,23 +156,25 @@ func (h *CertHandler) RenewCertificate(c *gin.Context) { return } - var cert config.Certificate - if err := config.DB.First(&cert, id).Error; err != nil { + cert := config.Store.GetByID(uint(id)) + if cert == nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } cert.Status = "renewing" - config.DB.Save(&cert) + config.Store.Upsert(cert) go func() { - if err := services.RenewCertificate(&cert, h.Cfg); err != nil { - cert.Status = "error" - cert.ErrorMessage = err.Error() + 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 { - cert.Status = "active" + saved.Status = "active" } - config.DB.Save(&cert) + config.Store.Upsert(saved) }() c.JSON(http.StatusAccepted, gin.H{"message": "renewal started", "certificate": cert}) @@ -165,13 +188,7 @@ func (h *CertHandler) DeleteCertificate(c *gin.Context) { 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 { + if err := config.Store.Delete(uint(id)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -187,8 +204,8 @@ func (h *CertHandler) UpdateCertificate(c *gin.Context) { return } - var cert config.Certificate - if err := config.DB.First(&cert, id).Error; err != nil { + cert := config.Store.GetByID(uint(id)) + if cert == nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } @@ -199,25 +216,19 @@ func (h *CertHandler) UpdateCertificate(c *gin.Context) { return } - // Only allow updating certain fields - allowedFields := map[string]bool{ - "auto_renew": true, - "renew_days": true, - "dns_config": true, + if v, ok := updates["auto_renew"]; ok { + cert.AutoRenew, _ = v.(bool) } - - filtered := make(map[string]interface{}) - for k, v := range updates { - if allowedFields[k] { - filtered[k] = v + if v, ok := updates["renew_days"]; ok { + if f, ok := v.(float64); ok { + cert.RenewDays = int(f) } } - - if err := config.DB.Model(&cert).Updates(filtered).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + if v, ok := updates["dns_config"]; ok { + cert.DNSConfig, _ = v.(string) } + config.Store.Upsert(cert) c.JSON(http.StatusOK, cert) } @@ -229,8 +240,8 @@ func (h *CertHandler) GetCertFiles(c *gin.Context) { return } - var cert config.Certificate - if err := config.DB.First(&cert, id).Error; err != nil { + cert := config.Store.GetByID(uint(id)) + if cert == nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } @@ -248,15 +259,14 @@ func (h *CertHandler) GetCertFiles(c *gin.Context) { // 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) + 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 { + 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) @@ -264,7 +274,7 @@ func (h *CertHandler) CheckRenewals(c *gin.Context) { cert.Status = "active" renewed = append(renewed, cert.Domain) } - config.DB.Save(&cert) + config.Store.Upsert(cert) } } @@ -277,11 +287,20 @@ func (h *CertHandler) CheckRenewals(c *gin.Context) { // 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) + 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, diff --git a/backend/main.go b/backend/main.go index f1c1a54..dfcc9c6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -16,8 +16,8 @@ import ( func main() { cfg := config.Load() - // Initialize database - config.InitDB(cfg) + // Initialize certificate store (JSON file persistence) + config.InitStore(cfg) // Setup Gin gin.SetMode(gin.ReleaseMode) @@ -62,20 +62,19 @@ func main() { c := cron.New() c.AddFunc("0 3 * * *", func() { log.Println("Running scheduled certificate renewal check...") - var certs []config.Certificate - config.DB.Where("auto_renew = ? AND status = ?", true, "active").Find(&certs) + certs := config.Store.GetActiveWithAutoRenew() for _, cert := range certs { if cert.ExpiresAt != nil && time.Until(*cert.ExpiresAt).Hours() < float64(cert.RenewDays*24) { log.Printf("Auto-renewing certificate for %s (expires %s)", cert.Domain, cert.ExpiresAt.Format(time.RFC3339)) - if err := services.RenewCertificate(&cert, cfg); err != nil { + if err := services.RenewCertificate(cert, cfg); err != nil { cert.Status = "error" cert.ErrorMessage = "auto renew: " + err.Error() log.Printf("Auto-renew failed for %s: %v", cert.Domain, err) } else { log.Printf("Auto-renew succeeded for %s", cert.Domain) } - config.DB.Save(&cert) + config.Store.Upsert(cert) } } })