- 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
154 lines
4.9 KiB
Go
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)
|
|
}
|