- 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
94 lines
2.3 KiB
Go
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
|
|
}
|