gohorsejobs/backend/internal/infrastructure/persistence/postgres/email_repository.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

154 lines
4.9 KiB
Go

package postgres
import (
"context"
"database/sql"
"encoding/json"
"errors"
"time"
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
)
type EmailRepository struct {
db *sql.DB
}
func NewEmailRepository(db *sql.DB) *EmailRepository {
return &EmailRepository{db: db}
}
// Templates
func (r *EmailRepository) ListTemplates(ctx context.Context) ([]*entity.EmailTemplate, error) {
query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates ORDER BY slug`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var templates []*entity.EmailTemplate
for rows.Next() {
t := &entity.EmailTemplate{}
var varsJSON []byte
if err := rows.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt); err != nil {
return nil, err
}
if len(varsJSON) > 0 {
json.Unmarshal(varsJSON, &t.Variables)
}
templates = append(templates, t)
}
return templates, nil
}
func (r *EmailRepository) GetTemplate(ctx context.Context, slug string) (*entity.EmailTemplate, error) {
query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates WHERE slug = $1`
row := r.db.QueryRowContext(ctx, query, slug)
t := &entity.EmailTemplate{}
var varsJSON []byte
err := row.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
if len(varsJSON) > 0 {
json.Unmarshal(varsJSON, &t.Variables)
}
return t, nil
}
func (r *EmailRepository) CreateTemplate(ctx context.Context, tmpl *entity.EmailTemplate) error {
query := `INSERT INTO email_templates (slug, subject, body_html, variables, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at`
varsJSON, _ := json.Marshal(tmpl.Variables)
if varsJSON == nil {
varsJSON = []byte("[]")
}
return r.db.QueryRowContext(ctx, query, tmpl.Slug, tmpl.Subject, tmpl.BodyHTML, varsJSON, time.Now()).Scan(&tmpl.ID, &tmpl.CreatedAt)
}
func (r *EmailRepository) UpdateTemplate(ctx context.Context, tmpl *entity.EmailTemplate) error {
query := `UPDATE email_templates SET subject=$1, body_html=$2, variables=$3, updated_at=$4 WHERE slug=$5`
varsJSON, _ := json.Marshal(tmpl.Variables)
if varsJSON == nil {
varsJSON = []byte("[]")
}
res, err := r.db.ExecContext(ctx, query, tmpl.Subject, tmpl.BodyHTML, varsJSON, time.Now(), tmpl.Slug)
if err != nil {
return err
}
affected, _ := res.RowsAffected()
if affected == 0 {
return errors.New("template not found")
}
return nil
}
func (r *EmailRepository) DeleteTemplate(ctx context.Context, slug string) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM email_templates WHERE slug=$1", slug)
return err
}
// Settings
func (r *EmailRepository) GetSettings(ctx context.Context) (*entity.EmailSettings, error) {
// We assume there's mostly one active setting, order by updated_at desc
query := `SELECT id, provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, sender_name, sender_email, amqp_url, is_active, updated_at
FROM email_settings WHERE is_active = true ORDER BY updated_at DESC LIMIT 1`
row := r.db.QueryRowContext(ctx, query)
s := &entity.EmailSettings{}
err := row.Scan(
&s.ID, &s.Provider, &s.SMTPHost, &s.SMTPPort, &s.SMTPUser, &s.SMTPPass,
&s.SMTPSecure, &s.SenderName, &s.SenderEmail, &s.AMQPURL, &s.IsActive, &s.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
// Return default empty struct or nil
return nil, nil
}
return nil, err
}
return s, nil
}
func (r *EmailRepository) UpdateSettings(ctx context.Context, s *entity.EmailSettings) error {
// We can either update the existing row or Insert a new one (audit history).
// Let's Insert a new one and set others to inactive if we want history, OR just update for simplicity as per requirement.
// Migration 027 says "id UUID PRIMARY KEY". Let's try to update if ID exists, else insert.
// Actually, GetSettings fetches the latest active.
// Let's implement logical "Upsert active settings".
// If ID is provided, update. If not, insert.
if s.ID != "" {
query := `UPDATE email_settings SET
provider=$1, smtp_host=$2, smtp_port=$3, smtp_user=$4, smtp_pass=$5, smtp_secure=$6,
sender_name=$7, sender_email=$8, amqp_url=$9, is_active=$10, updated_at=$11
WHERE id=$12`
_, err := r.db.ExecContext(ctx, query,
s.Provider, s.SMTPHost, s.SMTPPort, s.SMTPUser, s.SMTPPass, s.SMTPSecure,
s.SenderName, s.SenderEmail, s.AMQPURL, s.IsActive, time.Now(), s.ID,
)
return err
}
// Insert new
query := `INSERT INTO email_settings (
provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure,
sender_name, sender_email, amqp_url, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`
return r.db.QueryRowContext(ctx, query,
s.Provider, s.SMTPHost, s.SMTPPort, s.SMTPUser, s.SMTPPass, s.SMTPSecure,
s.SenderName, s.SenderEmail, s.AMQPURL, true,
).Scan(&s.ID)
}