Building out notification update channels
Some checks are pending
check / check (push) Waiting to run
Some checks are pending
check / check (push) Waiting to run
This commit is contained in:
101
backend-go/internal/handlers/email_digest.go
Normal file
101
backend-go/internal/handlers/email_digest.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/middleware"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/models"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/services"
|
||||
)
|
||||
|
||||
type EmailDigestHandler struct {
|
||||
digestSvc *services.EmailDigestService
|
||||
configs *models.NotificationConfigModel
|
||||
}
|
||||
|
||||
func NewEmailDigestHandler(
|
||||
digestSvc *services.EmailDigestService,
|
||||
configs *models.NotificationConfigModel,
|
||||
) *EmailDigestHandler {
|
||||
return &EmailDigestHandler{digestSvc: digestSvc, configs: configs}
|
||||
}
|
||||
|
||||
// SetupEmail reads the user's email from their notification config and sends
|
||||
// a confirmation message via Resend to verify the integration works.
|
||||
func (h *EmailDigestHandler) SetupEmail(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
if !h.digestSvc.Configured() {
|
||||
writeError(w, http.StatusServiceUnavailable, "EMAIL_NOT_CONFIGURED",
|
||||
"Email service is not configured on the server")
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := h.configs.GetConfig(r.Context(), userID)
|
||||
if err != nil {
|
||||
log.Printf("Error getting notification config for email setup: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR",
|
||||
"Failed to load notification config")
|
||||
return
|
||||
}
|
||||
|
||||
if cfg == nil || cfg.Email == nil || *cfg.Email == "" {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR",
|
||||
"Save an email address in notification settings first")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.digestSvc.SetupEmail(*cfg.Email); err != nil {
|
||||
log.Printf("Email setup failed for user %s: %v", userID, err)
|
||||
writeError(w, http.StatusBadGateway, "EMAIL_SEND_FAILED",
|
||||
"Failed to send confirmation email — check server email config")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Email setup confirmation sent to user %s (%s)", userID, *cfg.Email)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"success": true,
|
||||
"email": *cfg.Email,
|
||||
"message": "Confirmation email sent",
|
||||
})
|
||||
}
|
||||
|
||||
// SendDigest compiles and sends a digest of recent alerts to the user's email.
|
||||
func (h *EmailDigestHandler) SendDigest(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
if !h.digestSvc.Configured() {
|
||||
writeError(w, http.StatusServiceUnavailable, "EMAIL_NOT_CONFIGURED",
|
||||
"Email service is not configured on the server")
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := h.configs.GetConfig(r.Context(), userID)
|
||||
if err != nil {
|
||||
log.Printf("Error getting notification config for digest: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR",
|
||||
"Failed to load notification config")
|
||||
return
|
||||
}
|
||||
|
||||
if cfg == nil || cfg.Email == nil || *cfg.Email == "" {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR",
|
||||
"No email address configured")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.digestSvc.SendDigest(r.Context(), userID, *cfg.Email); err != nil {
|
||||
log.Printf("Digest send failed for user %s: %v", userID, err)
|
||||
writeError(w, http.StatusBadGateway, "DIGEST_SEND_FAILED",
|
||||
"Failed to send digest email")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Digest sent to user %s (%s)", userID, *cfg.Email)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"success": true,
|
||||
"email": *cfg.Email,
|
||||
"message": "Digest email sent",
|
||||
})
|
||||
}
|
||||
@@ -7,19 +7,22 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/config"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/domain"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/middleware"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/models"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/notifications"
|
||||
)
|
||||
|
||||
var emailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
|
||||
|
||||
type NotificationConfigHandler struct {
|
||||
configs *models.NotificationConfigModel
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewNotificationConfigHandler(configs *models.NotificationConfigModel) *NotificationConfigHandler {
|
||||
return &NotificationConfigHandler{configs: configs}
|
||||
func NewNotificationConfigHandler(configs *models.NotificationConfigModel, cfg *config.Config) *NotificationConfigHandler {
|
||||
return &NotificationConfigHandler{configs: configs, cfg: cfg}
|
||||
}
|
||||
|
||||
func (h *NotificationConfigHandler) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -51,7 +54,9 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.
|
||||
var body struct {
|
||||
DiscordWebhookURL *string `json:"discord_webhook_url"`
|
||||
TelegramChatID *string `json:"telegram_chat_id"`
|
||||
TelegramBotToken *string `json:"telegram_bot_token"`
|
||||
Email *string `json:"email"`
|
||||
SlackWebhookURL *string `json:"slack_webhook_url"`
|
||||
NotificationEnabled *bool `json:"notification_enabled"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
@@ -63,7 +68,8 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.
|
||||
log.Printf("User %s updating notification config", userID)
|
||||
|
||||
if body.DiscordWebhookURL == nil && body.TelegramChatID == nil &&
|
||||
body.Email == nil && body.NotificationEnabled == nil {
|
||||
body.TelegramBotToken == nil && body.Email == nil &&
|
||||
body.SlackWebhookURL == nil && body.NotificationEnabled == nil {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR",
|
||||
"At least one configuration field must be provided")
|
||||
return
|
||||
@@ -76,6 +82,13 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
if body.SlackWebhookURL != nil && *body.SlackWebhookURL != "" &&
|
||||
!strings.HasPrefix(*body.SlackWebhookURL, "https://hooks.slack.com/") {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR",
|
||||
"Invalid Slack webhook URL format")
|
||||
return
|
||||
}
|
||||
|
||||
if body.Email != nil && *body.Email != "" && !emailRe.MatchString(*body.Email) {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR",
|
||||
"Invalid email address format")
|
||||
@@ -90,7 +103,9 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.
|
||||
cfg := domain.NotificationConfig{
|
||||
DiscordWebhookURL: body.DiscordWebhookURL,
|
||||
TelegramChatID: body.TelegramChatID,
|
||||
TelegramBotToken: body.TelegramBotToken,
|
||||
Email: body.Email,
|
||||
SlackWebhookURL: body.SlackWebhookURL,
|
||||
NotificationEnabled: enabled,
|
||||
}
|
||||
|
||||
@@ -126,3 +141,76 @@ func (h *NotificationConfigHandler) DeleteConfig(w http.ResponseWriter, r *http.
|
||||
log.Println("Notification config deleted")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// TestChannels sends a test message to all configured notification channels.
|
||||
func (h *NotificationConfigHandler) TestChannels(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
log.Printf("User %s testing notification channels", userID)
|
||||
|
||||
cfg, err := h.configs.GetConfig(r.Context(), userID)
|
||||
if err != nil {
|
||||
log.Printf("Error getting notification config for test: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to get notification config")
|
||||
return
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
writeError(w, http.StatusNotFound, "NOT_FOUND", "No notification configuration found")
|
||||
return
|
||||
}
|
||||
|
||||
type channelResult struct {
|
||||
Channel string `json:"channel"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var results []channelResult
|
||||
|
||||
if cfg.DiscordWebhookURL != nil && *cfg.DiscordWebhookURL != "" {
|
||||
ok, testErr := notifications.TestDiscordWebhook(*cfg.DiscordWebhookURL)
|
||||
res := channelResult{Channel: "discord", Success: ok}
|
||||
if testErr != nil {
|
||||
res.Error = testErr.Error()
|
||||
}
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
if cfg.TelegramBotToken != nil && *cfg.TelegramBotToken != "" &&
|
||||
cfg.TelegramChatID != nil && *cfg.TelegramChatID != "" {
|
||||
ok, testErr := notifications.TestTelegramWebhook(*cfg.TelegramBotToken, *cfg.TelegramChatID)
|
||||
res := channelResult{Channel: "telegram", Success: ok}
|
||||
if testErr != nil {
|
||||
res.Error = testErr.Error()
|
||||
}
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
if cfg.SlackWebhookURL != nil && *cfg.SlackWebhookURL != "" {
|
||||
ok, testErr := notifications.TestSlackWebhook(*cfg.SlackWebhookURL)
|
||||
res := channelResult{Channel: "slack", Success: ok}
|
||||
if testErr != nil {
|
||||
res.Error = testErr.Error()
|
||||
}
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
if cfg.Email != nil && *cfg.Email != "" {
|
||||
ok, testErr := notifications.TestEmailNotification(
|
||||
h.cfg.ResendAPIKey, h.cfg.EmailFrom, *cfg.Email,
|
||||
)
|
||||
res := channelResult{Channel: "email", Success: ok}
|
||||
if testErr != nil {
|
||||
res.Error = testErr.Error()
|
||||
}
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "NO_CHANNELS",
|
||||
"No notification channels are configured")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"results": results})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user