Complete rewrite of Django REST API to Go with: - Gin web framework for HTTP routing - GORM for database operations - GoAdmin for admin panel - Gorush integration for push notifications - Redis for caching and job queues Features implemented: - User authentication (login, register, logout, password reset) - Residence management (CRUD, sharing, share codes) - Task management (CRUD, kanban board, completions) - Contractor management (CRUD, specialties) - Document management (CRUD, warranties) - Notifications (preferences, push notifications) - Subscription management (tiers, limits) Infrastructure: - Docker Compose for local development - Database migrations and seed data - Admin panel for data management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
306 lines
9.0 KiB
Go
306 lines
9.0 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
"gopkg.in/gomail.v2"
|
|
|
|
"github.com/treytartt/mycrib-api/internal/config"
|
|
)
|
|
|
|
// EmailService handles sending emails
|
|
type EmailService struct {
|
|
cfg *config.EmailConfig
|
|
dialer *gomail.Dialer
|
|
}
|
|
|
|
// NewEmailService creates a new email service
|
|
func NewEmailService(cfg *config.EmailConfig) *EmailService {
|
|
dialer := gomail.NewDialer(cfg.Host, cfg.Port, cfg.User, cfg.Password)
|
|
|
|
return &EmailService{
|
|
cfg: cfg,
|
|
dialer: dialer,
|
|
}
|
|
}
|
|
|
|
// SendEmail sends an email
|
|
func (s *EmailService) SendEmail(to, subject, htmlBody, textBody string) error {
|
|
m := gomail.NewMessage()
|
|
m.SetHeader("From", s.cfg.From)
|
|
m.SetHeader("To", to)
|
|
m.SetHeader("Subject", subject)
|
|
m.SetBody("text/plain", textBody)
|
|
m.AddAlternative("text/html", htmlBody)
|
|
|
|
if err := s.dialer.DialAndSend(m); err != nil {
|
|
log.Error().Err(err).Str("to", to).Str("subject", subject).Msg("Failed to send email")
|
|
return fmt.Errorf("failed to send email: %w", err)
|
|
}
|
|
|
|
log.Info().Str("to", to).Str("subject", subject).Msg("Email sent successfully")
|
|
return nil
|
|
}
|
|
|
|
// SendWelcomeEmail sends a welcome email with verification code
|
|
func (s *EmailService) SendWelcomeEmail(to, firstName, code string) error {
|
|
subject := "Welcome to MyCrib - Verify Your Email"
|
|
|
|
name := firstName
|
|
if name == "" {
|
|
name = "there"
|
|
}
|
|
|
|
htmlBody := fmt.Sprintf(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { text-align: center; padding: 20px 0; }
|
|
.code { background: #f4f4f4; padding: 20px; text-align: center; font-size: 32px; font-weight: bold; letter-spacing: 8px; border-radius: 8px; margin: 20px 0; }
|
|
.footer { text-align: center; color: #666; font-size: 12px; margin-top: 40px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Welcome to MyCrib!</h1>
|
|
</div>
|
|
<p>Hi %s,</p>
|
|
<p>Thank you for creating a MyCrib account. To complete your registration, please verify your email address by entering the following code:</p>
|
|
<div class="code">%s</div>
|
|
<p>This code will expire in 24 hours.</p>
|
|
<p>If you didn't create a MyCrib account, you can safely ignore this email.</p>
|
|
<p>Best regards,<br>The MyCrib Team</p>
|
|
<div class="footer">
|
|
<p>© %d MyCrib. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`, name, code, time.Now().Year())
|
|
|
|
textBody := fmt.Sprintf(`
|
|
Welcome to MyCrib!
|
|
|
|
Hi %s,
|
|
|
|
Thank you for creating a MyCrib account. To complete your registration, please verify your email address by entering the following code:
|
|
|
|
%s
|
|
|
|
This code will expire in 24 hours.
|
|
|
|
If you didn't create a MyCrib account, you can safely ignore this email.
|
|
|
|
Best regards,
|
|
The MyCrib Team
|
|
`, name, code)
|
|
|
|
return s.SendEmail(to, subject, htmlBody, textBody)
|
|
}
|
|
|
|
// SendVerificationEmail sends an email verification code
|
|
func (s *EmailService) SendVerificationEmail(to, firstName, code string) error {
|
|
subject := "MyCrib - Verify Your Email"
|
|
|
|
name := firstName
|
|
if name == "" {
|
|
name = "there"
|
|
}
|
|
|
|
htmlBody := fmt.Sprintf(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.code { background: #f4f4f4; padding: 20px; text-align: center; font-size: 32px; font-weight: bold; letter-spacing: 8px; border-radius: 8px; margin: 20px 0; }
|
|
.footer { text-align: center; color: #666; font-size: 12px; margin-top: 40px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Verify Your Email</h1>
|
|
<p>Hi %s,</p>
|
|
<p>Please use the following code to verify your email address:</p>
|
|
<div class="code">%s</div>
|
|
<p>This code will expire in 24 hours.</p>
|
|
<p>If you didn't request this, you can safely ignore this email.</p>
|
|
<p>Best regards,<br>The MyCrib Team</p>
|
|
<div class="footer">
|
|
<p>© %d MyCrib. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`, name, code, time.Now().Year())
|
|
|
|
textBody := fmt.Sprintf(`
|
|
Verify Your Email
|
|
|
|
Hi %s,
|
|
|
|
Please use the following code to verify your email address:
|
|
|
|
%s
|
|
|
|
This code will expire in 24 hours.
|
|
|
|
If you didn't request this, you can safely ignore this email.
|
|
|
|
Best regards,
|
|
The MyCrib Team
|
|
`, name, code)
|
|
|
|
return s.SendEmail(to, subject, htmlBody, textBody)
|
|
}
|
|
|
|
// SendPasswordResetEmail sends a password reset email
|
|
func (s *EmailService) SendPasswordResetEmail(to, firstName, code string) error {
|
|
subject := "MyCrib - Password Reset Request"
|
|
|
|
name := firstName
|
|
if name == "" {
|
|
name = "there"
|
|
}
|
|
|
|
htmlBody := fmt.Sprintf(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.code { background: #f4f4f4; padding: 20px; text-align: center; font-size: 32px; font-weight: bold; letter-spacing: 8px; border-radius: 8px; margin: 20px 0; }
|
|
.warning { background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 8px; margin: 20px 0; }
|
|
.footer { text-align: center; color: #666; font-size: 12px; margin-top: 40px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Password Reset Request</h1>
|
|
<p>Hi %s,</p>
|
|
<p>We received a request to reset your password. Use the following code to complete the reset:</p>
|
|
<div class="code">%s</div>
|
|
<p>This code will expire in 15 minutes.</p>
|
|
<div class="warning">
|
|
<strong>Security Notice:</strong> If you didn't request a password reset, please ignore this email. Your password will remain unchanged.
|
|
</div>
|
|
<p>Best regards,<br>The MyCrib Team</p>
|
|
<div class="footer">
|
|
<p>© %d MyCrib. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`, name, code, time.Now().Year())
|
|
|
|
textBody := fmt.Sprintf(`
|
|
Password Reset Request
|
|
|
|
Hi %s,
|
|
|
|
We received a request to reset your password. Use the following code to complete the reset:
|
|
|
|
%s
|
|
|
|
This code will expire in 15 minutes.
|
|
|
|
SECURITY NOTICE: If you didn't request a password reset, please ignore this email. Your password will remain unchanged.
|
|
|
|
Best regards,
|
|
The MyCrib Team
|
|
`, name, code)
|
|
|
|
return s.SendEmail(to, subject, htmlBody, textBody)
|
|
}
|
|
|
|
// SendPasswordChangedEmail sends a password changed confirmation email
|
|
func (s *EmailService) SendPasswordChangedEmail(to, firstName string) error {
|
|
subject := "MyCrib - Your Password Has Been Changed"
|
|
|
|
name := firstName
|
|
if name == "" {
|
|
name = "there"
|
|
}
|
|
|
|
htmlBody := fmt.Sprintf(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.warning { background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 8px; margin: 20px 0; }
|
|
.footer { text-align: center; color: #666; font-size: 12px; margin-top: 40px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Password Changed</h1>
|
|
<p>Hi %s,</p>
|
|
<p>Your MyCrib password was successfully changed on %s.</p>
|
|
<div class="warning">
|
|
<strong>Didn't make this change?</strong> If you didn't change your password, please contact us immediately at support@mycrib.com or reset your password.
|
|
</div>
|
|
<p>Best regards,<br>The MyCrib Team</p>
|
|
<div class="footer">
|
|
<p>© %d MyCrib. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`, name, time.Now().UTC().Format("January 2, 2006 at 3:04 PM UTC"), time.Now().Year())
|
|
|
|
textBody := fmt.Sprintf(`
|
|
Password Changed
|
|
|
|
Hi %s,
|
|
|
|
Your MyCrib password was successfully changed on %s.
|
|
|
|
DIDN'T MAKE THIS CHANGE? If you didn't change your password, please contact us immediately at support@mycrib.com or reset your password.
|
|
|
|
Best regards,
|
|
The MyCrib Team
|
|
`, name, time.Now().UTC().Format("January 2, 2006 at 3:04 PM UTC"))
|
|
|
|
return s.SendEmail(to, subject, htmlBody, textBody)
|
|
}
|
|
|
|
// EmailTemplate represents an email template
|
|
type EmailTemplate struct {
|
|
name string
|
|
template *template.Template
|
|
}
|
|
|
|
// ParseTemplate parses an email template from a string
|
|
func ParseTemplate(name, tmpl string) (*EmailTemplate, error) {
|
|
t, err := template.New(name).Parse(tmpl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &EmailTemplate{name: name, template: t}, nil
|
|
}
|
|
|
|
// Execute executes the template with the given data
|
|
func (t *EmailTemplate) Execute(data interface{}) (string, error) {
|
|
var buf bytes.Buffer
|
|
if err := t.template.Execute(&buf, data); err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|