Add email logging and SMTP health check
- Add structured logging for email sending (success/failure) - Add HealthCheck method to EmailService interface - Extend /health endpoint to include SMTP status - Update all email service mocks to implement HealthCheck - SMTP status shows: ok, error, or disabled
This commit is contained in:
+1
-1
@@ -116,7 +116,7 @@ func main() {
|
|||||||
authHandlers := httpInfra.NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator)
|
authHandlers := httpInfra.NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator)
|
||||||
habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator)
|
habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator)
|
||||||
statsHandlers := httpInfra.NewStatsHandlers(getHabitStatsHandler, translator)
|
statsHandlers := httpInfra.NewStatsHandlers(getHabitStatsHandler, translator)
|
||||||
healthHandlers := httpInfra.NewHealthHandlers(db.Conn())
|
healthHandlers := httpInfra.NewHealthHandlers(db.Conn(), emailService)
|
||||||
userHandlers := httpInfra.NewUserHandlers(deleteUserHandler, translator)
|
userHandlers := httpInfra.NewUserHandlers(deleteUserHandler, translator)
|
||||||
|
|
||||||
router := httpInfra.NewRouter(cfg.AppURL, habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator)
|
router := httpInfra.NewRouter(cfg.AppURL, habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator)
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ func (m *mockRequestResetEmailService) Send(message services.EmailMessage) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockRequestResetEmailService) HealthCheck() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestRequestPasswordResetHandler_Success(t *testing.T) {
|
func TestRequestPasswordResetHandler_Success(t *testing.T) {
|
||||||
user := entities.NewUser("test@example.com", "hash")
|
user := entities.NewUser("test@example.com", "hash")
|
||||||
user.ID = "user-123"
|
user.ID = "user-123"
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ func (m *mockEmailService) Send(message services.EmailMessage) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockEmailService) HealthCheck() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestVerifyEmailHandler_Success(t *testing.T) {
|
func TestVerifyEmailHandler_Success(t *testing.T) {
|
||||||
token := "valid-token"
|
token := "valid-token"
|
||||||
expiry := time.Now().Add(24 * time.Hour)
|
expiry := time.Now().Add(24 * time.Hour)
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ type EmailMessage struct {
|
|||||||
|
|
||||||
type EmailService interface {
|
type EmailService interface {
|
||||||
Send(message EmailMessage) error
|
Send(message EmailMessage) error
|
||||||
|
HealthCheck() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package email
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -53,9 +54,11 @@ func (s *SMTPService) Send(message services.EmailMessage) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.sendWithRetry(dialer, m); err != nil {
|
if err := s.sendWithRetry(dialer, m); err != nil {
|
||||||
|
log.Printf("[EMAIL] status=failed to=%s subject=%q error=%q", message.To, message.Subject, err.Error())
|
||||||
return fmt.Errorf("failed to send email: %w", err)
|
return fmt.Errorf("failed to send email: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[EMAIL] status=sent to=%s subject=%q", message.To, message.Subject)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,3 +106,28 @@ func isConfigError(err error) bool {
|
|||||||
func (s *SMTPService) GetConfig() SMTPConfig {
|
func (s *SMTPService) GetConfig() SMTPConfig {
|
||||||
return s.config
|
return s.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SMTPService) HealthCheck() error {
|
||||||
|
dialer := mail.NewDialer(s.config.Host, s.config.Port, s.config.Username, s.config.Password)
|
||||||
|
dialer.TLSConfig = &tls.Config{
|
||||||
|
ServerName: s.config.Host,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.config.Port == 465 {
|
||||||
|
dialer.SSL = true
|
||||||
|
}
|
||||||
|
|
||||||
|
smtpCloser, err := dialer.Dial()
|
||||||
|
if err != nil {
|
||||||
|
if isAuthError(err) {
|
||||||
|
return fmt.Errorf("SMTP authentication failed: %w", err)
|
||||||
|
}
|
||||||
|
if isConfigError(err) {
|
||||||
|
return fmt.Errorf("SMTP connection failed: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("SMTP error: %w", err)
|
||||||
|
}
|
||||||
|
defer smtpCloser.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/services"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,17 +11,20 @@ var startTime = time.Now()
|
|||||||
|
|
||||||
type HealthHandlers struct {
|
type HealthHandlers struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
|
emailService services.EmailService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHealthHandlers(db *sql.DB) *HealthHandlers {
|
func NewHealthHandlers(db *sql.DB, emailService services.EmailService) *HealthHandlers {
|
||||||
return &HealthHandlers{
|
return &HealthHandlers{
|
||||||
db: db,
|
db: db,
|
||||||
|
emailService: emailService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type HealthResponse struct {
|
type HealthResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Database string `json:"database"`
|
Database string `json:"database"`
|
||||||
|
SMTP string `json:"smtp"`
|
||||||
Uptime string `json:"uptime"`
|
Uptime string `json:"uptime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +38,7 @@ type HealthResponse struct {
|
|||||||
// @Router /health [get]
|
// @Router /health [get]
|
||||||
func (h *HealthHandlers) Health(w http.ResponseWriter, r *http.Request) {
|
func (h *HealthHandlers) Health(w http.ResponseWriter, r *http.Request) {
|
||||||
dbStatus := "ok"
|
dbStatus := "ok"
|
||||||
|
smtpStatus := "ok"
|
||||||
overallStatus := "ok"
|
overallStatus := "ok"
|
||||||
statusCode := http.StatusOK
|
statusCode := http.StatusOK
|
||||||
|
|
||||||
@@ -43,12 +48,25 @@ func (h *HealthHandlers) Health(w http.ResponseWriter, r *http.Request) {
|
|||||||
statusCode = http.StatusServiceUnavailable
|
statusCode = http.StatusServiceUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.emailService != nil {
|
||||||
|
if err := h.emailService.HealthCheck(); err != nil {
|
||||||
|
smtpStatus = "error"
|
||||||
|
if overallStatus != "degraded" {
|
||||||
|
overallStatus = "degraded"
|
||||||
|
statusCode = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
smtpStatus = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
uptime := time.Since(startTime)
|
uptime := time.Since(startTime)
|
||||||
uptimeStr := formatDuration(uptime)
|
uptimeStr := formatDuration(uptime)
|
||||||
|
|
||||||
response := HealthResponse{
|
response := HealthResponse{
|
||||||
Status: overallStatus,
|
Status: overallStatus,
|
||||||
Database: dbStatus,
|
Database: dbStatus,
|
||||||
|
SMTP: smtpStatus,
|
||||||
Uptime: uptimeStr,
|
Uptime: uptimeStr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func setupTestServer(t *testing.T) *TestServer {
|
|||||||
authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator)
|
authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator)
|
||||||
habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator)
|
habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator)
|
||||||
statsHandlers := NewStatsHandlers(getHabitStatsHandler, translator)
|
statsHandlers := NewStatsHandlers(getHabitStatsHandler, translator)
|
||||||
healthHandlers := NewHealthHandlers(db)
|
healthHandlers := NewHealthHandlers(db, nil)
|
||||||
userHandlers := NewUserHandlers(deleteUserHandler, translator)
|
userHandlers := NewUserHandlers(deleteUserHandler, translator)
|
||||||
|
|
||||||
router := NewRouter("http://localhost:3000", habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator)
|
router := NewRouter("http://localhost:3000", habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator)
|
||||||
|
|||||||
Reference in New Issue
Block a user