gohorsejobs/backend/internal/services/email_service.go
Tiago Yamamoto 841b1d780c feat: Email System, Avatar Upload, Email Templates UI, and Public Job Posting
- Backend: Email producer (LavinMQ), EmailService interface
- Backend: CRUD API for email_templates and email_settings
- Backend: avatar_url field in users table + UpdateMyProfile support
- Backend: StorageService for pre-signed URLs
- NestJS: Email consumer with Nodemailer and Handlebars
- Frontend: Email Templates admin pages (list/edit)
- Frontend: Updated profileApi.uploadAvatar with pre-signed URL flow
- Frontend: New /post-job public page (company registration + job creation wizard)
- Migrations: 027_create_email_system.sql, 028_add_avatar_url_to_users.sql
2025-12-26 12:21:34 -03:00

94 lines
2.3 KiB
Go

package services
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
type EmailService struct {
db *sql.DB
credentialsService *CredentialsService
}
func NewEmailService(db *sql.DB, cs *CredentialsService) *EmailService {
return &EmailService{
db: db,
credentialsService: cs,
} // Ensure return pointer matches
}
type EmailJob struct {
To string `json:"to"`
Template string `json:"template"` // slug
Variables map[string]interface{} `json:"variables"`
}
// SendTemplateEmail queues an email via RabbitMQ
func (s *EmailService) SendTemplateEmail(ctx context.Context, to, templateSlug string, variables map[string]interface{}) error {
// 1. Get AMQP URL from email_settings
var amqpURL sql.NullString
err := s.db.QueryRowContext(ctx, "SELECT amqp_url FROM email_settings LIMIT 1").Scan(&amqpURL)
if err != nil && err != sql.ErrNoRows {
log.Printf("[EmailService] Failed to fetch AMQP URL: %v", err)
}
url := ""
if amqpURL.Valid {
url = amqpURL.String
}
if url == "" {
// Log but don't error hard if just testing, but for "system" we need IT.
// Return error so caller knows.
return fmt.Errorf("AMQP URL not configured in email_settings")
}
// 2. Connect & Publish
conn, err := amqp.Dial(url)
if err != nil {
return fmt.Errorf("failed to connect to RabbitMQ: %w", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
return fmt.Errorf("failed to open channel: %w", err)
}
defer ch.Close()
q, err := ch.QueueDeclare(
"mail_queue", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
return fmt.Errorf("failed to declare queue: %w", err)
}
job := EmailJob{To: to, Template: templateSlug, Variables: variables}
body, _ := json.Marshal(job)
err = ch.PublishWithContext(ctx,
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: body,
})
if err != nil {
return fmt.Errorf("failed to publish message: %w", err)
}
log.Printf("[EmailService] Queued email to %s (Template: %s)", to, templateSlug)
return nil
}