package handlers import ( "net/http" "strings" "github.com/flatrender/notification-svc/internal/db" "github.com/flatrender/notification-svc/internal/middleware" "github.com/flatrender/notification-svc/internal/models" "github.com/flatrender/notification-svc/internal/sender" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type ChannelHandler struct{ store *db.Store } func NewChannelHandler(s *db.Store) *ChannelHandler { return &ChannelHandler{store: s} } func (h *ChannelHandler) tenant(c *gin.Context) uuid.UUID { v, _ := c.Get(middleware.CtxTenantID) id, _ := v.(uuid.UUID) return id } func (h *ChannelHandler) user(c *gin.Context) uuid.UUID { v, _ := c.Get(middleware.CtxUserID) id, _ := v.(uuid.UUID) return id } // GET /v1/channels — both provider configs, secrets masked. func (h *ChannelHandler) ListConfigs(c *gin.Context) { cfgs, err := h.store.GetChannelConfigs(c.Request.Context(), h.tenant(c)) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } out := map[string]*models.ChannelConfig{ "sms": {Channel: "sms", Settings: map[string]any{}}, "email": {Channel: "email", Settings: map[string]any{}}, } for _, cf := range cfgs { out[cf.Channel] = cf } mask(out["sms"], "api_key") mask(out["email"], "password") c.JSON(http.StatusOK, gin.H{"sms": out["sms"], "email": out["email"]}) } // PUT /v1/channels/:channel — upsert provider settings (blank secret keeps existing). func (h *ChannelHandler) UpsertConfig(c *gin.Context) { channel := c.Param("channel") if channel != "sms" && channel != "email" { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "channel must be sms or email"}) return } var req models.UpsertChannelConfigRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } settings := req.Settings if settings == nil { settings = map[string]any{} } enabled := false if req.Enabled != nil { enabled = *req.Enabled } secretKey := "api_key" if channel == "email" { secretKey = "password" } // Preserve the secret if the client sent it blank/omitted (it's masked on read). existing, _ := h.store.GetChannelConfig(c.Request.Context(), h.tenant(c), channel) if existing != nil { if v, ok := settings[secretKey]; !ok || str(v) == "" || strings.Contains(str(v), "•") { if ev, ok := existing.Settings[secretKey]; ok { settings[secretKey] = ev } } } if err := h.store.UpsertChannelConfig(c.Request.Context(), h.tenant(c), channel, settings, enabled); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // POST /v1/sms/send — send an SMS via Kavenegar. func (h *ChannelHandler) SendSMS(c *gin.Context) { var req models.SendSMSRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } cfg, _ := h.store.GetChannelConfig(c.Request.Context(), h.tenant(c), "sms") if cfg == nil || !cfg.Enabled { c.JSON(http.StatusBadRequest, models.APIError{Code: "not_configured", Message: "SMS channel is not configured/enabled"}) return } msgID, err := sender.SendSMS(str(cfg.Settings["api_key"]), str(cfg.Settings["line_number"]), req.To, req.Message) status, errMsg := "Sent", (*string)(nil) if err != nil { status = "Failed" e := err.Error() errMsg = &e } provider := "kavenegar" _ = h.store.LogDelivery(c.Request.Context(), h.tenant(c), h.user(c), "SMS", req.To, nil, &provider, ptr(msgID), &status, errMsg) if err != nil { c.JSON(http.StatusBadGateway, models.APIError{Code: "send_failed", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "sent", "message_id": msgID}) } // POST /v1/email/send — send an email via SMTP (optionally from a template + variables). func (h *ChannelHandler) SendEmail(c *gin.Context) { var req models.SendEmailRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } subject, html := req.Subject, req.BodyHTML if req.TemplateCode != "" { locale := req.Locale if locale == "" { locale = "fa" } tpl, _ := h.store.GetEmailTemplate(c.Request.Context(), req.TemplateCode, locale) if tpl == nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "email template not found"}) return } if tpl.Subject != nil { subject = *tpl.Subject } if tpl.BodyHTML != nil { html = *tpl.BodyHTML } for k, v := range req.Variables { subject = strings.ReplaceAll(subject, "{{"+k+"}}", v) html = strings.ReplaceAll(html, "{{"+k+"}}", v) } } cfg, _ := h.store.GetChannelConfig(c.Request.Context(), h.tenant(c), "email") if cfg == nil || !cfg.Enabled { c.JSON(http.StatusBadRequest, models.APIError{Code: "not_configured", Message: "Email channel is not configured/enabled"}) return } scfg := sender.SMTPConfig{ Host: str(cfg.Settings["host"]), Port: intv(cfg.Settings["port"]), Username: str(cfg.Settings["username"]), Password: str(cfg.Settings["password"]), FromEmail: str(cfg.Settings["from_email"]), FromName: str(cfg.Settings["from_name"]), UseTLS: boolv(cfg.Settings["use_tls"]), } err := sender.SendEmail(scfg, req.To, subject, html) status, errMsg := "Sent", (*string)(nil) if err != nil { status = "Failed" e := err.Error() errMsg = &e } provider := "smtp" _ = h.store.LogDelivery(c.Request.Context(), h.tenant(c), h.user(c), "Email", req.To, &subject, &provider, nil, &status, errMsg) if err != nil { c.JSON(http.StatusBadGateway, models.APIError{Code: "send_failed", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "sent"}) } // ── helpers ────────────────────────────────────────────────────────────────── func mask(cfg *models.ChannelConfig, key string) { if cfg == nil || cfg.Settings == nil { return } if v, ok := cfg.Settings[key]; ok && str(v) != "" { s := str(v) if len(s) > 4 { cfg.Settings[key] = "••••••••" + s[len(s)-4:] } else { cfg.Settings[key] = "••••" } } } func str(v any) string { if s, ok := v.(string); ok { return s } return "" } func intv(v any) int { switch n := v.(type) { case float64: return int(n) case int: return n } return 0 } func boolv(v any) bool { b, _ := v.(bool) return b } func ptr(s string) *string { if s == "" { return nil } return &s }