mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-09 10:57:04 +00:00
Backend: - Add IsEncryptionEnabled() method to ConfigPersistence - Include encryption status in /api/notifications/health response - Allows frontend to warn when credentials are stored in plaintext Frontend: - Update NotificationHealth type to include encryption.enabled field - Frontend can now display warnings when encryption is disabled This addresses the P2 requirement for encryption visibility, allowing operators to know when notification credentials are not encrypted at rest.
755 lines
24 KiB
Go
755 lines
24 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// NotificationHandlers handles notification-related HTTP endpoints
|
|
type NotificationHandlers struct {
|
|
monitor *monitoring.Monitor
|
|
}
|
|
|
|
// NewNotificationHandlers creates new notification handlers
|
|
func NewNotificationHandlers(monitor *monitoring.Monitor) *NotificationHandlers {
|
|
return &NotificationHandlers{
|
|
monitor: monitor,
|
|
}
|
|
}
|
|
|
|
// SetMonitor updates the monitor reference for notification handlers.
|
|
func (h *NotificationHandlers) SetMonitor(m *monitoring.Monitor) {
|
|
h.monitor = m
|
|
}
|
|
|
|
// GetEmailConfig returns the current email configuration
|
|
func (h *NotificationHandlers) GetEmailConfig(w http.ResponseWriter, r *http.Request) {
|
|
config := h.monitor.GetNotificationManager().GetEmailConfig()
|
|
|
|
// For security, don't return the password
|
|
config.Password = ""
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(config)
|
|
}
|
|
|
|
// UpdateEmailConfig updates the email configuration
|
|
func (h *NotificationHandlers) UpdateEmailConfig(w http.ResponseWriter, r *http.Request) {
|
|
// Read raw body for debugging
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// NEVER log the body as it contains passwords
|
|
log.Info().
|
|
Msg("Received email config update")
|
|
|
|
var config notifications.EmailConfig
|
|
if err := json.Unmarshal(body, &config); err != nil {
|
|
log.Error().Err(err).Msg("Failed to parse email config") // Don't log body with passwords
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// If password is empty, preserve the existing password
|
|
if config.Password == "" {
|
|
existingConfig := h.monitor.GetNotificationManager().GetEmailConfig()
|
|
config.Password = existingConfig.Password
|
|
}
|
|
|
|
log.Info().
|
|
Bool("enabled", config.Enabled).
|
|
Str("smtp", config.SMTPHost).
|
|
Str("from", config.From).
|
|
Int("toCount", len(config.To)).
|
|
Bool("hasPassword", config.Password != "").
|
|
Msg("Parsed email config")
|
|
|
|
h.monitor.GetNotificationManager().SetEmailConfig(config)
|
|
|
|
// Save to persistent storage
|
|
if err := h.monitor.GetConfigPersistence().SaveEmailConfig(config); err != nil {
|
|
// Log error but don't fail the request
|
|
log.Error().Err(err).Msg("Failed to save email configuration")
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
|
}
|
|
|
|
// GetAppriseConfig returns the current Apprise configuration.
|
|
func (h *NotificationHandlers) GetAppriseConfig(w http.ResponseWriter, r *http.Request) {
|
|
config := h.monitor.GetNotificationManager().GetAppriseConfig()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(config); err != nil {
|
|
log.Error().Err(err).Msg("Failed to encode Apprise configuration response")
|
|
}
|
|
}
|
|
|
|
// UpdateAppriseConfig updates the Apprise configuration.
|
|
func (h *NotificationHandlers) UpdateAppriseConfig(w http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var config notifications.AppriseConfig
|
|
if err := json.Unmarshal(body, &config); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
log.Info().
|
|
Bool("enabled", config.Enabled).
|
|
Str("mode", string(config.Mode)).
|
|
Int("targetCount", len(config.Targets)).
|
|
Str("cliPath", config.CLIPath).
|
|
Str("serverUrl", config.ServerURL).
|
|
Str("configKey", config.ConfigKey).
|
|
Bool("hasApiKey", config.APIKey != "").
|
|
Str("apiKeyHeader", config.APIKeyHeader).
|
|
Bool("skipTlsVerify", config.SkipTLSVerify).
|
|
Int("timeoutSeconds", config.TimeoutSeconds).
|
|
Msg("Parsed Apprise configuration update")
|
|
|
|
h.monitor.GetNotificationManager().SetAppriseConfig(config)
|
|
|
|
if err := h.monitor.GetConfigPersistence().SaveAppriseConfig(config); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save Apprise configuration")
|
|
}
|
|
|
|
normalized := h.monitor.GetNotificationManager().GetAppriseConfig()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(normalized); err != nil {
|
|
log.Error().Err(err).Msg("Failed to encode Apprise configuration response")
|
|
}
|
|
}
|
|
|
|
// GetWebhooks returns all webhook configurations with secrets masked
|
|
func (h *NotificationHandlers) GetWebhooks(w http.ResponseWriter, r *http.Request) {
|
|
webhooks := h.monitor.GetNotificationManager().GetWebhooks()
|
|
|
|
// Mask sensitive fields in headers and customFields
|
|
maskedWebhooks := make([]map[string]interface{}, len(webhooks))
|
|
for i, webhook := range webhooks {
|
|
whMap := map[string]interface{}{
|
|
"id": webhook.ID,
|
|
"name": webhook.Name,
|
|
"url": webhook.URL,
|
|
"method": webhook.Method,
|
|
"enabled": webhook.Enabled,
|
|
"service": webhook.Service,
|
|
}
|
|
|
|
// Mask headers - only show keys, not values
|
|
if webhook.Headers != nil && len(webhook.Headers) > 0 {
|
|
maskedHeaders := make(map[string]string)
|
|
for key := range webhook.Headers {
|
|
maskedHeaders[key] = "***REDACTED***"
|
|
}
|
|
whMap["headers"] = maskedHeaders
|
|
}
|
|
|
|
// Mask custom fields - only show keys, not values
|
|
if webhook.CustomFields != nil && len(webhook.CustomFields) > 0 {
|
|
maskedFields := make(map[string]string)
|
|
for key := range webhook.CustomFields {
|
|
maskedFields[key] = "***REDACTED***"
|
|
}
|
|
whMap["customFields"] = maskedFields
|
|
}
|
|
|
|
// Include template if present
|
|
if webhook.Template != "" {
|
|
whMap["template"] = webhook.Template
|
|
}
|
|
|
|
maskedWebhooks[i] = whMap
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(maskedWebhooks)
|
|
}
|
|
|
|
// CreateWebhook creates a new webhook
|
|
func (h *NotificationHandlers) CreateWebhook(w http.ResponseWriter, r *http.Request) {
|
|
// Read the raw body to preserve all fields
|
|
bodyBytes, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var webhook notifications.WebhookConfig
|
|
if err := json.Unmarshal(bodyBytes, &webhook); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate webhook URL
|
|
if err := notifications.ValidateWebhookURL(webhook.URL); err != nil {
|
|
http.Error(w, fmt.Sprintf("Invalid webhook URL: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Generate ID if not provided
|
|
if webhook.ID == "" {
|
|
webhook.ID = utils.GenerateID("webhook")
|
|
}
|
|
|
|
h.monitor.GetNotificationManager().AddWebhook(webhook)
|
|
|
|
// Save webhooks to persistent storage with all fields
|
|
webhooks := h.monitor.GetNotificationManager().GetWebhooks()
|
|
if err := h.monitor.GetConfigPersistence().SaveWebhooks(webhooks); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save webhooks")
|
|
}
|
|
|
|
// Return the full webhook data including any extra fields like 'service'
|
|
var responseData map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &responseData); err != nil {
|
|
log.Warn().Err(err).Msg("Failed to unmarshal webhook response data")
|
|
responseData = make(map[string]interface{})
|
|
}
|
|
responseData["id"] = webhook.ID
|
|
|
|
if err := utils.WriteJSONResponse(w, responseData); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write webhook creation response")
|
|
}
|
|
}
|
|
|
|
// UpdateWebhook updates an existing webhook
|
|
func (h *NotificationHandlers) UpdateWebhook(w http.ResponseWriter, r *http.Request) {
|
|
// Extract webhook ID from URL path
|
|
// Path is like /api/notifications/webhooks/{id} after routing
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/notifications/webhooks/")
|
|
webhookID := path
|
|
|
|
if webhookID == "" {
|
|
http.Error(w, "Invalid URL - missing webhook ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Read the raw body to preserve all fields
|
|
bodyBytes, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var webhook notifications.WebhookConfig
|
|
if err := json.Unmarshal(bodyBytes, &webhook); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate webhook URL
|
|
if err := notifications.ValidateWebhookURL(webhook.URL); err != nil {
|
|
http.Error(w, fmt.Sprintf("Invalid webhook URL: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
webhook.ID = webhookID
|
|
if err := h.monitor.GetNotificationManager().UpdateWebhook(webhookID, webhook); err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Save webhooks to persistent storage
|
|
webhooks := h.monitor.GetNotificationManager().GetWebhooks()
|
|
if err := h.monitor.GetConfigPersistence().SaveWebhooks(webhooks); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save webhooks")
|
|
}
|
|
|
|
// Return the full webhook data including any extra fields like 'service'
|
|
var responseData map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &responseData); err != nil {
|
|
log.Warn().Err(err).Msg("Failed to unmarshal webhook response data")
|
|
responseData = make(map[string]interface{})
|
|
}
|
|
responseData["id"] = webhookID
|
|
|
|
if err := utils.WriteJSONResponse(w, responseData); err != nil {
|
|
log.Error().Err(err).Str("webhookID", webhookID).Msg("Failed to write webhook update response")
|
|
}
|
|
}
|
|
|
|
// DeleteWebhook deletes a webhook
|
|
func (h *NotificationHandlers) DeleteWebhook(w http.ResponseWriter, r *http.Request) {
|
|
// Extract webhook ID from URL path
|
|
// Path is like /api/notifications/webhooks/{id} after routing
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/notifications/webhooks/")
|
|
webhookID := path
|
|
|
|
if webhookID == "" {
|
|
http.Error(w, "Invalid URL - missing webhook ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.monitor.GetNotificationManager().DeleteWebhook(webhookID); err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Save webhooks to persistent storage
|
|
webhooks := h.monitor.GetNotificationManager().GetWebhooks()
|
|
if err := h.monitor.GetConfigPersistence().SaveWebhooks(webhooks); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save webhooks")
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, map[string]string{"status": "success"}); err != nil {
|
|
log.Error().Err(err).Str("webhookID", webhookID).Msg("Failed to write webhook deletion response")
|
|
}
|
|
}
|
|
|
|
// TestNotification sends a test notification
|
|
func (h *NotificationHandlers) TestNotification(w http.ResponseWriter, r *http.Request) {
|
|
// Read body for debugging
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// NEVER log the body as it contains passwords
|
|
log.Info().
|
|
Msg("Test notification request received")
|
|
|
|
var req struct {
|
|
Method string `json:"method"` // "email" or "webhook"
|
|
Type string `json:"type"` // Alternative field name used by frontend
|
|
Config *notifications.EmailConfig `json:"config,omitempty"` // Optional config for testing
|
|
WebhookID string `json:"webhookId,omitempty"` // For webhook testing
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Support both "method" and "type" field names
|
|
if req.Method == "" && req.Type != "" {
|
|
req.Method = req.Type
|
|
}
|
|
|
|
// Get actual node info from monitor state
|
|
state := h.monitor.GetState()
|
|
var nodeInfo *notifications.TestNodeInfo
|
|
|
|
// Use first available node and instance
|
|
if len(state.Nodes) > 0 {
|
|
for _, node := range state.Nodes {
|
|
nodeInfo = ¬ifications.TestNodeInfo{
|
|
NodeName: node.Name,
|
|
InstanceURL: node.Instance,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Handle webhook testing
|
|
if req.Method == "webhook" && req.WebhookID != "" {
|
|
log.Info().
|
|
Str("webhookId", req.WebhookID).
|
|
Msg("Testing specific webhook")
|
|
|
|
// Get the webhook by ID and test it
|
|
webhooks := h.monitor.GetNotificationManager().GetWebhooks()
|
|
var foundWebhook *notifications.WebhookConfig
|
|
for _, wh := range webhooks {
|
|
if wh.ID == req.WebhookID {
|
|
foundWebhook = &wh
|
|
break
|
|
}
|
|
}
|
|
|
|
if foundWebhook == nil {
|
|
http.Error(w, "Webhook not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Send test webhook
|
|
if err := h.monitor.GetNotificationManager().SendTestWebhook(*foundWebhook); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else if req.Method == "email" && req.Config != nil {
|
|
// If config is provided, use it for testing (without saving)
|
|
// If password is empty, use the saved password
|
|
if req.Config.Password == "" {
|
|
savedConfig := h.monitor.GetNotificationManager().GetEmailConfig()
|
|
req.Config.Password = savedConfig.Password
|
|
}
|
|
|
|
log.Info().
|
|
Bool("enabled", req.Config.Enabled).
|
|
Str("smtp", req.Config.SMTPHost).
|
|
Str("from", req.Config.From).
|
|
Int("toCount", len(req.Config.To)).
|
|
Strs("to", req.Config.To).
|
|
Bool("hasPassword", req.Config.Password != "").
|
|
Msg("Testing email with provided config")
|
|
|
|
if err := h.monitor.GetNotificationManager().SendTestNotificationWithConfig(req.Method, req.Config, nodeInfo); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else {
|
|
// Use saved config
|
|
if err := h.monitor.GetNotificationManager().SendTestNotification(req.Method); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "Test notification sent"})
|
|
}
|
|
|
|
// GetWebhookTemplates returns available webhook templates
|
|
func (h *NotificationHandlers) GetWebhookTemplates(w http.ResponseWriter, r *http.Request) {
|
|
templates := notifications.GetWebhookTemplates()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(templates)
|
|
}
|
|
|
|
// GetWebhookHistory returns recent webhook delivery history with URLs redacted
|
|
func (h *NotificationHandlers) GetWebhookHistory(w http.ResponseWriter, r *http.Request) {
|
|
history := h.monitor.GetNotificationManager().GetWebhookHistory()
|
|
|
|
// Redact secrets from URLs in history
|
|
for i := range history {
|
|
history[i].WebhookURL = redactSecretsFromURL(history[i].WebhookURL)
|
|
// Note: ResponseBody is not stored in WebhookDelivery struct
|
|
// Error messages are already limited in length
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(history)
|
|
}
|
|
|
|
// redactSecretsFromURL masks tokens and credentials in URLs
|
|
func redactSecretsFromURL(urlStr string) string {
|
|
// Redact common patterns like:
|
|
// - /bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/sendMessage → /botXXX:REDACTED/sendMessage
|
|
// - ?token=abc123 → ?token=REDACTED
|
|
// - ?apikey=abc123 → ?apikey=REDACTED
|
|
|
|
// Redact Telegram bot tokens
|
|
if idx := strings.Index(urlStr, "/bot"); idx != -1 {
|
|
if endIdx := strings.Index(urlStr[idx:], "/"); endIdx != -1 {
|
|
urlStr = urlStr[:idx+4] + "REDACTED" + urlStr[idx+endIdx:]
|
|
}
|
|
}
|
|
|
|
// Redact query parameters with sensitive names
|
|
if idx := strings.Index(urlStr, "?"); idx != -1 {
|
|
sensitiveParams := []string{"token", "apikey", "api_key", "key", "secret", "password"}
|
|
for _, param := range sensitiveParams {
|
|
pattern := param + "="
|
|
if paramIdx := strings.Index(urlStr, pattern); paramIdx != -1 {
|
|
start := paramIdx + len(pattern)
|
|
end := start
|
|
for end < len(urlStr) && urlStr[end] != '&' && urlStr[end] != '#' {
|
|
end++
|
|
}
|
|
urlStr = urlStr[:start] + "REDACTED" + urlStr[end:]
|
|
}
|
|
}
|
|
}
|
|
|
|
return urlStr
|
|
}
|
|
|
|
// GetEmailProviders returns available email providers
|
|
func (h *NotificationHandlers) GetEmailProviders(w http.ResponseWriter, r *http.Request) {
|
|
providers := notifications.GetEmailProviders()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(providers)
|
|
}
|
|
|
|
// TestWebhook tests a webhook configuration
|
|
func (h *NotificationHandlers) TestWebhook(w http.ResponseWriter, r *http.Request) {
|
|
// First try to decode as basic webhook config
|
|
var basicWebhook notifications.WebhookConfig
|
|
bodyBytes, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := json.Unmarshal(bodyBytes, &basicWebhook); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Convert to enhanced webhook for testing
|
|
webhook := notifications.EnhancedWebhookConfig{
|
|
WebhookConfig: basicWebhook,
|
|
Service: "generic", // Default to generic if not specified
|
|
RetryEnabled: false, // Don't retry during testing
|
|
}
|
|
|
|
if len(basicWebhook.CustomFields) > 0 {
|
|
customFields := make(map[string]interface{}, len(basicWebhook.CustomFields))
|
|
for key, value := range basicWebhook.CustomFields {
|
|
customFields[key] = value
|
|
}
|
|
webhook.CustomFields = customFields
|
|
}
|
|
|
|
// If the webhook has a custom template, use it
|
|
if basicWebhook.Template != "" {
|
|
webhook.PayloadTemplate = basicWebhook.Template
|
|
}
|
|
|
|
// Try to extract service from body if present
|
|
var serviceCheck struct {
|
|
Service string `json:"service"`
|
|
}
|
|
if err := json.Unmarshal(bodyBytes, &serviceCheck); err == nil && serviceCheck.Service != "" {
|
|
webhook.Service = serviceCheck.Service
|
|
// Also set it in the basic webhook for consistency
|
|
basicWebhook.Service = serviceCheck.Service
|
|
webhook.WebhookConfig.Service = serviceCheck.Service
|
|
}
|
|
|
|
log.Info().
|
|
Str("service", webhook.Service).
|
|
Str("url", webhook.URL).
|
|
Str("name", webhook.Name).
|
|
Msg("Testing webhook")
|
|
|
|
// Get template for the service (if not using custom template)
|
|
if webhook.PayloadTemplate == "" {
|
|
templates := notifications.GetWebhookTemplates()
|
|
for _, tmpl := range templates {
|
|
if tmpl.Service == webhook.Service {
|
|
webhook.PayloadTemplate = tmpl.PayloadTemplate
|
|
if webhook.Headers == nil {
|
|
webhook.Headers = make(map[string]string)
|
|
}
|
|
// Only copy headers that don't contain template syntax
|
|
// This prevents issues with headers that have Go template expressions
|
|
for k, v := range tmpl.Headers {
|
|
if !strings.Contains(v, "{{") {
|
|
webhook.Headers[k] = v
|
|
}
|
|
}
|
|
log.Info().Str("service", webhook.Service).Msg("Found template for service")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If still no template found, use a simple generic template
|
|
if webhook.PayloadTemplate == "" {
|
|
webhook.PayloadTemplate = `{
|
|
"alert": {
|
|
"id": "{{.ID}}",
|
|
"type": "{{.Type}}",
|
|
"level": "{{.Level}}",
|
|
"resourceName": "{{.ResourceName}}",
|
|
"node": "{{.Node}}",
|
|
"message": "{{.Message}}",
|
|
"value": {{.Value}},
|
|
"threshold": {{.Threshold}}
|
|
},
|
|
"source": "pulse-monitoring",
|
|
"timestamp": {{.Timestamp}}
|
|
}`
|
|
}
|
|
|
|
// Test the webhook
|
|
status, response, err := h.monitor.GetNotificationManager().TestEnhancedWebhook(webhook)
|
|
|
|
result := map[string]interface{}{
|
|
"status": status,
|
|
"response": response,
|
|
}
|
|
|
|
if err != nil {
|
|
result["error"] = err.Error()
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
} else if status < 200 || status >= 300 {
|
|
// HTTP error from webhook endpoint
|
|
result["error"] = fmt.Sprintf("Webhook returned HTTP %d: %s", status, response)
|
|
result["success"] = false
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
} else {
|
|
result["success"] = true
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(result)
|
|
}
|
|
|
|
// GetNotificationHealth returns health status of notification system
|
|
func (h *NotificationHandlers) GetNotificationHealth(w http.ResponseWriter, r *http.Request) {
|
|
// Get queue stats
|
|
queueStats := make(map[string]interface{})
|
|
if queue := h.monitor.GetNotificationManager().GetQueue(); queue != nil {
|
|
stats, err := queue.GetQueueStats()
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("Failed to get queue stats for health check")
|
|
queueStats["error"] = err.Error()
|
|
queueStats["healthy"] = false
|
|
} else {
|
|
queueStats = map[string]interface{}{
|
|
"pending": stats["pending"],
|
|
"sending": stats["sending"],
|
|
"sent": stats["sent"],
|
|
"failed": stats["failed"],
|
|
"dlq": stats["dlq"],
|
|
"healthy": true,
|
|
}
|
|
}
|
|
} else {
|
|
queueStats["error"] = "queue not initialized"
|
|
queueStats["healthy"] = false
|
|
}
|
|
|
|
// Get config status
|
|
nm := h.monitor.GetNotificationManager()
|
|
emailCfg := nm.GetEmailConfig()
|
|
webhooks := nm.GetWebhooks()
|
|
|
|
health := map[string]interface{}{
|
|
"queue": queueStats,
|
|
"email": map[string]interface{}{
|
|
"enabled": emailCfg.Enabled,
|
|
"configured": emailCfg.SMTPHost != "",
|
|
},
|
|
"webhooks": map[string]interface{}{
|
|
"total": len(webhooks),
|
|
"enabled": countEnabledWebhooks(webhooks),
|
|
},
|
|
"encryption": map[string]interface{}{
|
|
"enabled": h.monitor.GetConfigPersistence().IsEncryptionEnabled(),
|
|
},
|
|
"overall_healthy": queueStats["healthy"] == true,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(health)
|
|
}
|
|
|
|
func countEnabledWebhooks(webhooks []notifications.WebhookConfig) int {
|
|
count := 0
|
|
for _, wh := range webhooks {
|
|
if wh.Enabled {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// HandleNotifications routes notification requests to appropriate handlers
|
|
func (h *NotificationHandlers) HandleNotifications(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/notifications")
|
|
|
|
requireAnyScope := func(required string, scopes ...string) bool {
|
|
record := getAPITokenRecordFromRequest(r)
|
|
if record == nil {
|
|
return true
|
|
}
|
|
for _, scope := range scopes {
|
|
if scope != "" && record.HasScope(scope) {
|
|
return true
|
|
}
|
|
}
|
|
respondMissingScope(w, required)
|
|
return false
|
|
}
|
|
|
|
switch {
|
|
case path == "/email" && r.Method == http.MethodGet:
|
|
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.GetEmailConfig(w, r)
|
|
case path == "/email" && r.Method == http.MethodPut:
|
|
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.UpdateEmailConfig(w, r)
|
|
case path == "/apprise" && r.Method == http.MethodGet:
|
|
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.GetAppriseConfig(w, r)
|
|
case path == "/apprise" && r.Method == http.MethodPut:
|
|
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.UpdateAppriseConfig(w, r)
|
|
case path == "/webhooks" && r.Method == http.MethodGet:
|
|
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.GetWebhooks(w, r)
|
|
case path == "/webhooks" && r.Method == http.MethodPost:
|
|
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.CreateWebhook(w, r)
|
|
case path == "/webhooks/test" && r.Method == http.MethodPost:
|
|
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.TestWebhook(w, r)
|
|
case strings.HasPrefix(path, "/webhooks/") && r.Method == http.MethodPut:
|
|
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.UpdateWebhook(w, r)
|
|
case strings.HasPrefix(path, "/webhooks/") && r.Method == http.MethodDelete:
|
|
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.DeleteWebhook(w, r)
|
|
case path == "/webhook-templates" && r.Method == http.MethodGet:
|
|
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.GetWebhookTemplates(w, r)
|
|
case path == "/webhook-history" && r.Method == http.MethodGet:
|
|
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.GetWebhookHistory(w, r)
|
|
case path == "/email-providers" && r.Method == http.MethodGet:
|
|
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.GetEmailProviders(w, r)
|
|
case path == "/test" && r.Method == http.MethodPost:
|
|
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.TestNotification(w, r)
|
|
case path == "/health" && r.Method == http.MethodGet:
|
|
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
|
return
|
|
}
|
|
h.GetNotificationHealth(w, r)
|
|
default:
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
}
|
|
}
|