From 2ad148f607da0c9e42052bbde3c2ad912a686cdb Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Fri, 6 Feb 2026 15:33:58 -0600 Subject: [PATCH] Refactor SendPulse into shared service, wire app registration to Sojorn Members list (book 568122) --- go-backend/cmd/api/main.go | 65 +------------ go-backend/internal/handlers/auth_handler.go | 17 +++- .../internal/services/sendpulse_service.go | 96 +++++++++++++++++++ 3 files changed, 112 insertions(+), 66 deletions(-) create mode 100644 go-backend/internal/services/sendpulse_service.go diff --git a/go-backend/cmd/api/main.go b/go-backend/cmd/api/main.go index 319e714..2807c93 100644 --- a/go-backend/cmd/api/main.go +++ b/go-backend/cmd/api/main.go @@ -1,11 +1,7 @@ package main import ( - "bytes" "context" - "encoding/json" - "fmt" - "io" "net/http" "os" "os/signal" @@ -116,6 +112,7 @@ func main() { notificationService := services.NewNotificationService(notifRepo, pushService, userRepo) emailService := services.NewEmailService(cfg) + sendPulseService := services.NewSendPulseService(cfg.SendPulseID, cfg.SendPulseSecret) // Load moderation configuration moderationConfig := config.NewModerationConfig() @@ -133,7 +130,7 @@ func main() { userHandler := handlers.NewUserHandler(userRepo, postRepo, notificationService, assetService) postHandler := handlers.NewPostHandler(postRepo, userRepo, feedService, assetService, notificationService, moderationService, contentFilter) chatHandler := handlers.NewChatHandler(chatRepo, notificationService, hub) - authHandler := handlers.NewAuthHandler(userRepo, cfg, emailService) + authHandler := handlers.NewAuthHandler(userRepo, cfg, emailService, sendPulseService) categoryHandler := handlers.NewCategoryHandler(categoryRepo) keyHandler := handlers.NewKeyHandler(userRepo) backupHandler := handlers.NewBackupHandler(repository.NewBackupRepository(dbPool)) @@ -200,10 +197,8 @@ func main() { c.JSON(500, gin.H{"error": "Failed to join waitlist"}) return } - // Add to SendPulse in background - go func(email string) { - addToSendPulse(cfg, email) - }(req.Email) + // Add to SendPulse waitlist in background + go sendPulseService.AddToWaitlist(req.Email) c.JSON(200, gin.H{"message": "You're on the list!"}) }) } @@ -466,55 +461,3 @@ func main() { log.Info().Msg("Server exiting") } - -func addToSendPulse(cfg *config.Config, email string) { - if cfg.SendPulseID == "" || cfg.SendPulseSecret == "" { - return - } - - // 1. Get OAuth token - tokenBody, _ := json.Marshal(map[string]string{ - "grant_type": "client_credentials", - "client_id": cfg.SendPulseID, - "client_secret": cfg.SendPulseSecret, - }) - tokenResp, err := http.Post("https://api.sendpulse.com/oauth/access_token", "application/json", bytes.NewReader(tokenBody)) - if err != nil { - log.Error().Err(err).Msg("SendPulse: failed to get token") - return - } - defer tokenResp.Body.Close() - - var tokenData struct { - AccessToken string `json:"access_token"` - } - if err := json.NewDecoder(tokenResp.Body).Decode(&tokenData); err != nil || tokenData.AccessToken == "" { - log.Error().Err(err).Msg("SendPulse: failed to parse token") - return - } - - // 2. Add subscriber to Sojorn Waitlist (address book 568090) - subBody, _ := json.Marshal(map[string]interface{}{ - "emails": []map[string]string{ - {"email": email}, - }, - }) - req, _ := http.NewRequest("POST", "https://api.sendpulse.com/addressbooks/568090/emails", bytes.NewReader(subBody)) - req.Header.Set("Authorization", "Bearer "+tokenData.AccessToken) - req.Header.Set("Content-Type", "application/json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Error().Err(err).Msg("SendPulse: failed to add subscriber") - return - } - defer resp.Body.Close() - - if resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) - log.Error().Str("status", fmt.Sprintf("%d", resp.StatusCode)).Str("body", string(body)).Msg("SendPulse: add subscriber failed") - return - } - - log.Info().Str("email", email).Msg("SendPulse: subscriber added to waitlist") -} diff --git a/go-backend/internal/handlers/auth_handler.go b/go-backend/internal/handlers/auth_handler.go index d09b201..099d06a 100644 --- a/go-backend/internal/handlers/auth_handler.go +++ b/go-backend/internal/handlers/auth_handler.go @@ -23,13 +23,14 @@ import ( ) type AuthHandler struct { - repo *repository.UserRepository - config *config.Config - emailService *services.EmailService + repo *repository.UserRepository + config *config.Config + emailService *services.EmailService + sendPulseService *services.SendPulseService } -func NewAuthHandler(repo *repository.UserRepository, cfg *config.Config, emailService *services.EmailService) *AuthHandler { - return &AuthHandler{repo: repo, config: cfg, emailService: emailService} +func NewAuthHandler(repo *repository.UserRepository, cfg *config.Config, emailService *services.EmailService, sendPulseService *services.SendPulseService) *AuthHandler { + return &AuthHandler{repo: repo, config: cfg, emailService: emailService, sendPulseService: sendPulseService} } type RegisterRequest struct { @@ -154,6 +155,12 @@ func (h *AuthHandler) Register(c *gin.Context) { log.Printf("[Auth] Failed to send email to %s: %v", req.Email, err) } }() + + // Add to SendPulse Members list if user opted into newsletter + if req.EmailNewsletter && h.sendPulseService != nil { + go h.sendPulseService.AddToMembers(req.Email) + } + c.JSON(http.StatusCreated, gin.H{ "message": "Registration successful. Please verify your email to activate your account.", "state": "verification_pending", diff --git a/go-backend/internal/services/sendpulse_service.go b/go-backend/internal/services/sendpulse_service.go new file mode 100644 index 0000000..21415b6 --- /dev/null +++ b/go-backend/internal/services/sendpulse_service.go @@ -0,0 +1,96 @@ +package services + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/rs/zerolog/log" +) + +const ( + SendPulseWaitlistBookID = 568090 + SendPulseMembersBookID = 568122 +) + +type SendPulseService struct { + ClientID string + ClientSecret string +} + +func NewSendPulseService(clientID, clientSecret string) *SendPulseService { + return &SendPulseService{ClientID: clientID, ClientSecret: clientSecret} +} + +func (s *SendPulseService) getToken() (string, error) { + tokenBody, _ := json.Marshal(map[string]string{ + "grant_type": "client_credentials", + "client_id": s.ClientID, + "client_secret": s.ClientSecret, + }) + resp, err := http.Post("https://api.sendpulse.com/oauth/access_token", "application/json", bytes.NewReader(tokenBody)) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var data struct { + AccessToken string `json:"access_token"` + } + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return "", err + } + if data.AccessToken == "" { + return "", fmt.Errorf("empty access token") + } + return data.AccessToken, nil +} + +func (s *SendPulseService) AddSubscriber(bookID int, email string) { + if s.ClientID == "" || s.ClientSecret == "" { + return + } + + token, err := s.getToken() + if err != nil { + log.Error().Err(err).Msg("SendPulse: failed to get token") + return + } + + subBody, _ := json.Marshal(map[string]interface{}{ + "emails": []map[string]string{ + {"email": email}, + }, + }) + url := fmt.Sprintf("https://api.sendpulse.com/addressbooks/%d/emails", bookID) + req, _ := http.NewRequest("POST", url, bytes.NewReader(subBody)) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Error().Err(err).Int("book_id", bookID).Msg("SendPulse: failed to add subscriber") + return + } + defer resp.Body.Close() + + if resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + log.Error().Int("status", resp.StatusCode).Str("body", string(body)).Int("book_id", bookID).Msg("SendPulse: add subscriber failed") + return + } + + log.Info().Str("email", email).Int("book_id", bookID).Msg("SendPulse: subscriber added") +} + +// AddToWaitlist adds an email to the Sojorn Waitlist +func (s *SendPulseService) AddToWaitlist(email string) { + s.AddSubscriber(SendPulseWaitlistBookID, email) +} + +// AddToMembers adds an email to the Sojorn Members list +func (s *SendPulseService) AddToMembers(email string) { + s.AddSubscriber(SendPulseMembersBookID, email) +}