feat: add Web UI for notification config (feishu + email)

- New config/notify_store.go: persistent notification config (notify.json)
- New API: GET/PUT /api/notify/config, POST /api/notify/test-feishu, POST /api/notify/test-email
- New Vue page: NotifyConfig.vue with feishu webhook and SMTP settings
- Updated cron/init to read from persistent store instead of env vars
- Password masked as ******** in API responses, preserved on updates
This commit is contained in:
2026-07-23 16:31:04 +08:00
parent 2d70a15307
commit bbf09fbc8d
9 changed files with 627 additions and 42 deletions
+30 -22
View File
@@ -1,6 +1,7 @@
package notify
import (
"auto-ssl/config"
"bytes"
"encoding/json"
"fmt"
@@ -16,18 +17,31 @@ import (
// Config holds notification configuration
type Config struct {
// Feishu webhook
FeishuWebhookURL string
// SMTP
SMTPHost string
SMTPPort string
SMTPUser string
SMTPPassword string
SMTPFrom string
NotifyTo string // comma-separated email recipients
SMTPHost string
SMTPPort string
SMTPUser string
SMTPPassword string
SMTPFrom string
NotifyTo string
ExpiryDays int
}
// LoadNotifyConfig reads notification config from environment
// ConfigFromStore converts store config to notify config
func ConfigFromStore(nc *config.NotifyConfig) *Config {
return &Config{
FeishuWebhookURL: nc.FeishuURL,
SMTPHost: nc.SMTPHost,
SMTPPort: nc.SMTPPort,
SMTPUser: nc.SMTPUser,
SMTPPassword: nc.SMTPPassword,
SMTPFrom: nc.SMTPFrom,
NotifyTo: nc.NotifyTo,
ExpiryDays: nc.ExpiryDays,
}
}
// LoadNotifyConfig reads notification config from environment (legacy fallback)
func LoadNotifyConfig() *Config {
return &Config{
FeishuWebhookURL: os.Getenv("FEISHU_WEBHOOK_URL"),
@@ -86,18 +100,18 @@ func (c *Config) NotifyCertExpiring(certs []CertInfo) error {
// Feishu message format
type feishuCard struct {
MsgType string `json:"msg_type"`
MsgType string `json:"msg_type"`
Card *feishuCardContent `json:"card"`
}
type feishuCardContent struct {
Header feishuHeader `json:"header"`
Elems []feishuElement `json:"elements"`
Header feishuHeader `json:"header"`
Elems []feishuElement `json:"elements"`
}
type feishuHeader struct {
Title feishuText `json:"title"`
Template string `json:"template"`
Title feishuText `json:"title"`
Template string `json:"template"`
}
type feishuText struct {
@@ -106,8 +120,8 @@ type feishuText struct {
}
type feishuElement struct {
Tag string `json:"tag"`
Text *feishuText `json:"text,omitempty"`
Tag string `json:"tag"`
Text *feishuText `json:"text,omitempty"`
Fields *[]feishuField `json:"fields,omitempty"`
}
@@ -117,7 +131,6 @@ type feishuField struct {
}
func (c *Config) sendFeishu(certs []CertInfo) error {
// Build card content
elements := []feishuElement{}
for _, cert := range certs {
@@ -150,7 +163,6 @@ func (c *Config) sendFeishu(certs []CertInfo) error {
)
}
// Summary
expiringCount := 0
for _, cert := range certs {
if cert.DaysLeft <= 7 {
@@ -158,7 +170,6 @@ func (c *Config) sendFeishu(certs []CertInfo) error {
}
}
// Determine template color
template := "blue"
if expiringCount > 0 {
template = "red"
@@ -202,7 +213,6 @@ func (c *Config) sendFeishu(certs []CertInfo) error {
}
func (c *Config) sendEmail(certs []CertInfo) error {
// Build HTML email
var sb strings.Builder
sb.WriteString(`
<html>
@@ -260,8 +270,6 @@ func (c *Config) sendEmail(certs []CertInfo) error {
`)
htmlContent := sb.String()
// Subject
subject := fmt.Sprintf("🔐 [AutoSSL] %d 个证书即将到期 - 请及时处理", len(certs))
e := email.NewEmail()