package main import ( "auto-ssl/auth" "auto-ssl/config" "auto-ssl/handlers" "auto-ssl/notify" "auto-ssl/services" "fmt" "log" "net/http" "os" "strings" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/robfig/cron/v3" ) func main() { cfg := config.Load() // Initialize all stores (certificates + notify config) config.InitAll(cfg) // Setup Gin gin.SetMode(gin.ReleaseMode) r := gin.Default() // CORS for Vue frontend r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, AllowCredentials: true, })) // Serve static files for frontend r.Static("/assets", "./dist/assets") r.StaticFile("/favicon.ico", "./dist/favicon.ico") r.StaticFile("/favicon.svg", "./dist/favicon.svg") r.StaticFile("/", "./dist/index.html") // Public routes (no auth required) r.POST("/api/login", handleLogin) r.GET("/api/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) // API routes with auth api := r.Group("/api") api.Use(authMiddleware()) { certHandler := handlers.NewCertHandler(cfg) credHandler := handlers.NewCredentialHandler() // Certificate management api.GET("/certificates", certHandler.ListCertificates) api.GET("/certificates/:id", certHandler.GetCertificate) api.POST("/certificates", certHandler.CreateCertificate) api.PUT("/certificates/:id", certHandler.UpdateCertificate) api.DELETE("/certificates/:id", certHandler.DeleteCertificate) api.POST("/certificates/:id/renew", certHandler.RenewCertificate) api.GET("/certificates/:id/files", certHandler.GetCertFiles) // Notification configuration api.GET("/notify/config", handleGetNotifyConfig) api.PUT("/notify/config", handleUpdateNotifyConfig) api.POST("/notify/test-feishu", handleTestFeishu) api.POST("/notify/test-email", handleTestEmail) // Credential management api.GET("/credentials", credHandler.ListCredentials) api.GET("/credentials/:id", credHandler.GetCredential) api.POST("/credentials", credHandler.CreateCredential) api.PUT("/credentials/:id", credHandler.UpdateCredential) api.DELETE("/credentials/:id", credHandler.DeleteCredential) api.GET("/credential-types", credHandler.GetCredentialTypes) // Utility api.GET("/renewals/check", certHandler.CheckRenewals) api.GET("/stats", certHandler.Stats) } r.NoRoute(func(c *gin.Context) { // 静态资源请求直接返回404,避免返回html导致MIME类型错误 if strings.HasPrefix(c.Request.URL.Path, "/assets/") || strings.HasSuffix(c.Request.URL.Path, ".js") || strings.HasSuffix(c.Request.URL.Path, ".css") || strings.HasSuffix(c.Request.URL.Path, ".png") || strings.HasSuffix(c.Request.URL.Path, ".svg") || strings.HasSuffix(c.Request.URL.Path, ".ico") { c.AbortWithStatus(http.StatusNotFound) return } // 其他路由返回index.html给前端处理 c.File("./dist/index.html") }) // Setup cron scheduler c := cron.New() // Daily renewal check at 3:00 AM c.AddFunc("0 3 * * *", func() { log.Println("Running scheduled certificate renewal check...") 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 { 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.Store.Upsert(cert) } } }) // Certificate expiry notification check at 8:00 AM daily c.AddFunc("0 8 * * *", func() { nc := config.NotifyCfg.GetConfig() notifyCfg := notify.ConfigFromStore(nc) if !notifyCfg.IsConfigured() { log.Println("Notification not configured, skipping expiry check notification") return } runExpiryNotification(notifyCfg) }) c.Start() // Set ACME HTTP-01 challenge port from env (default 8082) acmePort := os.Getenv("ACME_PORT") if acmePort == "" { acmePort = "8082" } services.SetHTTP01Port(acmePort) log.Printf("ACME HTTP-01 challenge port: %s (nginx should proxy .well-known/acme-challenge/ to this port)", acmePort) log.Printf("AutoSSL server starting on :%s", cfg.Port) if err := r.Run(":" + cfg.Port); err != nil { log.Fatalf("Failed to start server: %v", err) } } // ===================== Login Handler ===================== func handleLogin(c *gin.Context) { var req auth.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请输入用户名和密码"}) return } if !auth.Authenticate(req.Username, req.Password) { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) return } token, expiresAt, err := auth.GenerateToken(req.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"}) return } c.JSON(http.StatusOK, auth.LoginResponse{ Token: token, Username: req.Username, ExpiresAt: expiresAt, }) } // ===================== Auth Middleware ===================== func authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { authHeader = c.Query("token") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未授权,请先登录"}) return } claims, err := auth.ValidateToken(authHeader) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token已过期或无效"}) return } c.Set("username", claims.Username) c.Next() return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") if tokenString == authHeader { tokenString = authHeader } claims, err := auth.ValidateToken(tokenString) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token已过期或无效"}) return } c.Set("username", claims.Username) c.Next() } } // ===================== Notification Config Handlers ===================== func handleGetNotifyConfig(c *gin.Context) { nc := config.NotifyCfg.Get() c.JSON(http.StatusOK, nc) } func handleUpdateNotifyConfig(c *gin.Context) { var req config.NotifyConfig if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := config.NotifyCfg.Update(&req); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "通知配置已更新"}) } func handleTestFeishu(c *gin.Context) { nc := config.NotifyCfg.GetConfig() if !nc.FeishuEnabled || nc.FeishuURL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "飞书通知未配置,请先保存配置"}) return } notifyCfg := notify.ConfigFromStore(nc) testCerts := []notify.CertInfo{ { Domain: "test.example.com", Status: "active", DaysLeft: 15, }, } if err := notifyCfg.NotifyCertExpiring(testCerts); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "飞书测试消息发送失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "飞书测试消息发送成功"}) } func handleTestEmail(c *gin.Context) { nc := config.NotifyCfg.GetConfig() if !nc.SMTPEnabled || nc.SMTPHost == "" || nc.NotifyTo == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "邮件通知未配置,请先保存配置"}) return } notifyCfg := notify.ConfigFromStore(nc) testCerts := []notify.CertInfo{ { Domain: "test.example.com", Status: "active", DaysLeft: 15, }, } if err := notifyCfg.NotifyCertExpiring(testCerts); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "邮件测试消息发送失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "邮件测试消息发送成功"}) } // ===================== Expiry Notification ===================== func runExpiryNotification(notifyCfg *notify.Config) { certs := config.Store.GetAll() var expiringCerts []notify.CertInfo nc := config.NotifyCfg.GetConfig() expiryDays := nc.ExpiryDays if expiryDays <= 0 { expiryDays = 30 } for _, cert := range certs { if cert.Status != "active" || cert.ExpiresAt == nil { continue } daysLeft := int(time.Until(*cert.ExpiresAt).Hours() / 24) if daysLeft <= expiryDays { info := notify.CertInfo{ Domain: cert.Domain, Status: cert.Status, ExpiresAt: cert.ExpiresAt, DaysLeft: daysLeft, } if daysLeft < 0 { info.Status = "expired" } expiringCerts = append(expiringCerts, info) } } if len(expiringCerts) > 0 { log.Printf("Found %d expiring certificates, sending notifications", len(expiringCerts)) if err := notifyCfg.NotifyCertExpiring(expiringCerts); err != nil { log.Printf("Failed to send expiry notifications: %v", err) } } else { log.Println("No expiring certificates found, skipping notification") } } func init() { // Run startup certificate expiry check after 10 seconds time.AfterFunc(10*time.Second, func() { if config.NotifyCfg != nil { nc := config.NotifyCfg.GetConfig() notifyCfg := notify.ConfigFromStore(nc) if notifyCfg.IsConfigured() { log.Println("Running startup certificate expiry check...") runExpiryNotification(notifyCfg) } } }) // Validate ADMIN_USER / ADMIN_PASSWORD env setting adminUser := os.Getenv("ADMIN_USER") adminPass := os.Getenv("ADMIN_PASSWORD") if adminUser != "" || adminPass != "" { log.Printf("Admin credentials configured via environment (user: %s)", adminUser) } _ = fmt.Sprintf }