Files
uptop/internal/alert/alert.go
T
lerko 4d5116644f fix(core): correctness and robustness fixes across all subsystems
- Move status page template to package-level template.Must (panic on
  parse error at init instead of nil deref at runtime)
- Fix XSS in import error responses (log detail server-side, return
  generic message to client)
- Handle ListenAndServe errors in HTTP and SSH servers
- Use defer resp.Body.Close() in all alert providers, check
  json.Marshal errors
- Share HTTP clients across checks instead of creating per-request
- Use http.NewRequestWithContext for per-site timeout control
- Support HTTP method field (was always GET despite DB storing method)
- Implement AcceptedCodes validation (was hardcoded >= 400 despite DB
  storing accepted code ranges)
- Add defer tx.Rollback() to ImportData for transaction safety
2026-05-15 00:00:02 -04:00

154 lines
3.4 KiB
Go

package alert
import (
"bytes"
"encoding/json"
"fmt"
"go-upkeep/internal/models"
"net/http"
"net/smtp"
"strings"
"time"
)
var alertClient = &http.Client{Timeout: 10 * time.Second}
type Provider interface {
Send(title, message string) error
}
func GetProvider(cfg models.AlertConfig) Provider {
switch cfg.Type {
case "discord":
return &DiscordProvider{URL: cfg.Settings["url"]}
case "slack":
return &SlackProvider{URL: cfg.Settings["url"]}
case "webhook":
// Generic Webhook
return &WebhookProvider{URL: cfg.Settings["url"]}
case "email":
port := "25"
if p, ok := cfg.Settings["port"]; ok {
port = p
}
return &EmailProvider{
Host: cfg.Settings["host"],
Port: port,
User: cfg.Settings["user"],
Pass: cfg.Settings["pass"],
To: cfg.Settings["to"],
From: cfg.Settings["from"],
}
case "ntfy":
priority := "3"
if p, ok := cfg.Settings["priority"]; ok && p != "" {
priority = p
}
return &NtfyProvider{
ServerURL: cfg.Settings["url"],
Topic: cfg.Settings["topic"],
Priority: priority,
Username: cfg.Settings["username"],
Password: cfg.Settings["password"],
}
default:
return nil
}
}
// --- DISCORD ---
type DiscordProvider struct{ URL string }
func (d *DiscordProvider) Send(title, message string) error {
payload := map[string]string{"content": fmt.Sprintf("**%s**\n%s", title, message)}
jsonValue, err := json.Marshal(payload)
if err != nil {
return err
}
resp, err := alertClient.Post(d.URL, "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// --- SLACK ---
type SlackProvider struct{ URL string }
func (s *SlackProvider) Send(title, message string) error {
payload := map[string]string{"text": fmt.Sprintf("*%s*\n%s", title, message)}
jsonValue, err := json.Marshal(payload)
if err != nil {
return err
}
resp, err := alertClient.Post(s.URL, "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// --- GENERIC WEBHOOK ---
type WebhookProvider struct{ URL string }
func (w *WebhookProvider) Send(title, message string) error {
payload := map[string]string{
"title": title,
"message": message,
"status": "alert",
}
jsonValue, err := json.Marshal(payload)
if err != nil {
return err
}
resp, err := alertClient.Post(w.URL, "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// --- EMAIL ---
type EmailProvider struct {
Host, Port, User, Pass, To, From string
}
func (e *EmailProvider) Send(title, message string) error {
auth := smtp.PlainAuth("", e.User, e.Pass, e.Host)
msg := []byte("To: " + e.To + "\r\n" +
"Subject: Go-Upkeep: " + title + "\r\n" +
"\r\n" +
message + "\r\n")
return smtp.SendMail(e.Host+":"+e.Port, auth, e.From, []string{e.To}, msg)
}
type NtfyProvider struct {
ServerURL string
Topic string
Priority string
Username string
Password string
}
func (n *NtfyProvider) Send(title, message string) error {
url := strings.TrimRight(n.ServerURL, "/") + "/" + n.Topic
req, err := http.NewRequest("POST", url, strings.NewReader(message))
if err != nil {
return err
}
req.Header.Set("Title", title)
req.Header.Set("Priority", n.Priority)
if n.Username != "" && n.Password != "" {
req.SetBasicAuth(n.Username, n.Password)
}
resp, err := alertClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}