improve coworker service

This commit is contained in:
Sergey Chubaryan 2025-02-17 17:32:44 +03:00
parent b4c1004cf2
commit 0db1564012
7 changed files with 181 additions and 101 deletions

View File

@ -16,7 +16,7 @@ type inputSendVerify struct {
func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
return httpserver.WrapGin(log,
func(ctx context.Context, input inputSendVerify) (interface{}, error) {
err := userService.SendEmailVerifyEmail(ctx, input.Email)
err := userService.SendEmailVerifyUser(ctx, input.Email)
return nil, err
},
)

30
cmd/coworker/config.go Normal file
View File

@ -0,0 +1,30 @@
package main
import "backend/pkg/config"
func LoadConfig(filePath string) (Config, error) {
return config.NewFromFile[Config](filePath)
}
type Config struct {
App struct {
LogFile string `yaml:"logFile"`
ServiceUrl string `yaml:"serviceUrl"`
}
Kafka struct {
Brokers []string `yaml:"brokers"`
Topic string `yaml:"topic"`
ConsumerGroupId string `yaml:"consumerGroupId"`
} `yaml:"kafka"`
SMTP ConfigSMTP `yaml:"smtp"`
}
type ConfigSMTP struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
Login string `yaml:"login"`
Password string `yaml:"password"`
Email string `yaml:"email"`
}

87
cmd/coworker/emailer.go Normal file
View File

@ -0,0 +1,87 @@
package main
import (
"html/template"
"strings"
"gopkg.in/gomail.v2"
)
const MSG_TEXT = `
<html>
<head>
</head>
<body>
<p>{{.Text}}</p>
{{if .Link}}
<a href="{{.Link}}">Click</a>link</p>
{{end}}
</body>
</html>
`
type MailContent struct {
Text string
Link string
}
func NewEmailer(conf ConfigSMTP) (*Emailer, error) {
dialer := gomail.NewDialer(conf.Server, conf.Port, conf.Login, conf.Password)
closer, err := dialer.Dial()
if err != nil {
return nil, err
}
defer closer.Close()
htmlTemplate, err := template.New("verify-email").Parse(MSG_TEXT)
if err != nil {
return nil, err
}
return &Emailer{
senderEmail: conf.Email,
htmlTemplate: htmlTemplate,
dialer: dialer,
}, nil
}
type Emailer struct {
senderEmail string
htmlTemplate *template.Template
dialer *gomail.Dialer
}
func (e *Emailer) SendRestorePassword(email, token string) error {
return e.sendEmail("Restore your password", email, MailContent{
Text: "Token: " + token,
})
}
func (e *Emailer) SendVerifyUser(email, link string) error {
return e.sendEmail("Verify your email", email, MailContent{
Text: "You recieved this message due to registration of account. Use this link to verify email:",
Link: link,
})
}
func (e *Emailer) SendPasswordChanged(email string) error {
return e.sendEmail("Password changed", email, MailContent{
Text: "You recieved this message due to password change",
})
}
func (e *Emailer) sendEmail(subject, to string, content MailContent) error {
builder := &strings.Builder{}
if err := e.htmlTemplate.Execute(builder, content); err != nil {
return err
}
m := gomail.NewMessage()
m.SetHeader("From", m.FormatAddress(e.senderEmail, "Pet Backend"))
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/html", builder.String())
return e.dialer.DialAndSend(m)
}

View File

@ -5,102 +5,46 @@ import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"os"
"strings"
"github.com/segmentio/kafka-go"
"gopkg.in/gomail.v2"
"gopkg.in/yaml.v3"
)
const MSG_TEXT = `
<html>
<head>
</head>
<body>
<p>This message was sent because you forgot a password</p>
<p>To change a password, use <a href="{{.Link}}">this</a>link</p>
</body>
</html>
`
type HtmlTemplate struct {
Link string
}
func SendEmailForgotPassword(dialer *gomail.Dialer, from, to, body string) error {
m := gomail.NewMessage()
m.SetHeader("From", m.FormatAddress(from, "Pet Backend"))
m.SetHeader("To", to)
m.SetHeader("Subject", "Hello!")
m.SetBody("text/html", body)
return dialer.DialAndSend(m)
}
type Config struct {
App struct {
LogFile string `yaml:"logFile"`
ServiceUrl string `yaml:"serviceUrl"`
}
Kafka struct {
Brokers []string `yaml:"brokers"`
Topic string `yaml:"topic"`
ConsumerGroupId string `yaml:"consumerGroupId"`
} `yaml:"kafka"`
SMTP struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
Login string `yaml:"login"`
Password string `yaml:"password"`
Email string `yaml:"email"`
} `yaml:"smtp"`
type SendEmailEvent struct {
Email string `json:"email"`
Token string `json:"token"`
}
func main() {
ctx := context.Background()
configFile, err := os.ReadFile("config.yaml")
config, err := LoadConfig("config.yaml")
if err != nil {
log.Fatal(err.Error())
}
config := &Config{}
if err := yaml.Unmarshal(configFile, config); err != nil {
emailer, err := NewEmailer(config.SMTP)
if err != nil {
log.Fatal(err.Error())
}
dialer := gomail.NewDialer(config.SMTP.Server, config.SMTP.Port, config.SMTP.Login, config.SMTP.Password)
r := kafka.NewReader(kafka.ReaderConfig{
Brokers: config.Kafka.Brokers,
Topic: config.Kafka.Topic,
GroupID: config.Kafka.ConsumerGroupId,
})
logger, err := logger.New(
ctx,
logger.NewLoggerOpts{
Debug: true,
OutputFile: config.App.LogFile,
},
)
logger, err := logger.New(ctx, logger.NewLoggerOpts{
Debug: true,
OutputFile: config.App.LogFile,
})
if err != nil {
log.Fatal(err.Error())
}
logger.Printf("coworker service started\n")
template, err := template.New("verify-email").Parse(MSG_TEXT)
if err != nil {
log.Fatal(err)
}
for {
msg, err := r.FetchMessage(ctx)
if err == io.EOF {
@ -119,27 +63,28 @@ func main() {
continue
}
value := struct {
Email string `json:"email"`
Token string `json:"token"`
}{}
if err := json.Unmarshal(msg.Value, &value); err != nil {
log.Fatalf("failed to unmarshal: %s\n", err.Error())
continue
}
link := fmt.Sprintf("%s/verify-user?token=%s", config.App.ServiceUrl, value.Token)
builder := &strings.Builder{}
if err := template.Execute(builder, HtmlTemplate{link}); err != nil {
log.Printf("failed to execute html template: %s\n", err.Error())
continue
}
if err := SendEmailForgotPassword(dialer, config.SMTP.Email, value.Email, builder.String()); err != nil {
log.Printf("failed to send email: %s\n", err.Error())
if err := handleEvent(config, emailer, msg); err != nil {
log.Printf("failed to handle event: %s\n", err.Error())
continue
}
}
}
func handleEvent(config Config, emailer *Emailer, msg kafka.Message) error {
event := SendEmailEvent{}
if err := json.Unmarshal(msg.Value, &event); err != nil {
return err
}
switch string(msg.Key) {
case "email_forgot_password":
return emailer.SendRestorePassword(event.Email, event.Token)
case "email_password_changed":
return emailer.SendPasswordChanged(event.Email)
case "email_verify_user":
link := fmt.Sprintf("%s/verify-user?token=%s", config.App.ServiceUrl, event.Token)
return emailer.SendVerifyUser(event.Email, link)
}
return fmt.Errorf("unknown event type")
}

View File

@ -6,6 +6,12 @@ import (
"encoding/json"
)
const (
EventEmailPasswordChanged = "email_password_changed"
EventEmailForgotPassword = "email_forgot_password"
EventEmailVerifyUser = "email_verify_user"
)
func NewEventRepo(kafka *integrations.Kafka) *EventRepo {
return &EventRepo{
kafka: kafka,
@ -32,10 +38,14 @@ func (e *EventRepo) sendEmail(ctx context.Context, email, actionToken, eventType
return e.kafka.SendMessage(ctx, eventType, valueBytes)
}
func (e *EventRepo) SendEmailForgotPassword(ctx context.Context, email, actionToken string) error {
return e.sendEmail(ctx, email, actionToken, "email_forgot_password")
func (e *EventRepo) SendEmailPasswordChanged(ctx context.Context, email string) error {
return e.sendEmail(ctx, email, "", EventEmailPasswordChanged)
}
func (e *EventRepo) SendEmailVerifyEmail(ctx context.Context, email, actionToken string) error {
return e.sendEmail(ctx, email, actionToken, "email_verify_email")
func (e *EventRepo) SendEmailForgotPassword(ctx context.Context, email, actionToken string) error {
return e.sendEmail(ctx, email, actionToken, EventEmailForgotPassword)
}
func (e *EventRepo) SendEmailVerifyUser(ctx context.Context, email, actionToken string) error {
return e.sendEmail(ctx, email, actionToken, EventEmailVerifyUser)
}

View File

@ -34,7 +34,7 @@ type UserService interface {
VerifyEmail(ctx context.Context, actionToken string) error
SendEmailForgotPassword(ctx context.Context, userId string) error
SendEmailVerifyEmail(ctx context.Context, email string) error
SendEmailVerifyUser(ctx context.Context, email string) error
ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error
ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error
@ -94,7 +94,7 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) (
return nil, err
}
if err := u.sendEmailVerifyEmail(ctx, result.Id, user.Email); err != nil {
if err := u.sendEmailVerifyUser(ctx, result.Id, user.Email); err != nil {
u.deps.Logger.Error().Err(err).Msg("error occured on sending email")
}
@ -162,7 +162,7 @@ func (u *userService) SendEmailForgotPassword(ctx context.Context, email string)
UserId: user.Id,
Value: uuid.New().String(),
Target: models.ActionTokenTargetForgotPassword,
Expiration: time.Now().Add(1 * time.Hour),
Expiration: time.Now().Add(15 * time.Minute),
},
)
if err != nil {
@ -172,7 +172,7 @@ func (u *userService) SendEmailForgotPassword(ctx context.Context, email string)
return u.deps.EventRepo.SendEmailForgotPassword(ctx, user.Email, actionToken.Value)
}
func (u *userService) sendEmailVerifyEmail(ctx context.Context, userId, email string) error {
func (u *userService) sendEmailVerifyUser(ctx context.Context, userId, email string) error {
actionToken, err := u.deps.ActionTokenRepo.CreateActionToken(
ctx,
models.ActionTokenDTO{
@ -186,10 +186,10 @@ func (u *userService) sendEmailVerifyEmail(ctx context.Context, userId, email st
return err
}
return u.deps.EventRepo.SendEmailVerifyEmail(ctx, email, actionToken.Value)
return u.deps.EventRepo.SendEmailVerifyUser(ctx, email, actionToken.Value)
}
func (u *userService) SendEmailVerifyEmail(ctx context.Context, email string) error {
func (u *userService) SendEmailVerifyUser(ctx context.Context, email string) error {
//user, err := u.getUserById(ctx, userId)
user, err := u.deps.UserRepo.GetUserByEmail(ctx, email)
if err != nil {
@ -202,7 +202,7 @@ func (u *userService) SendEmailVerifyEmail(ctx context.Context, email string) er
return fmt.Errorf("user already verified")
}
return u.sendEmailVerifyEmail(ctx, user.Id, user.Email)
return u.sendEmailVerifyUser(ctx, user.Id, user.Email)
}
func (u *userService) ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error {
@ -256,10 +256,18 @@ func (u *userService) updatePassword(ctx context.Context, user models.UserDTO, n
return err
}
return u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{
if err = u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{
Secret: newSecret,
Name: user.Name,
})
}); err != nil {
return err
}
if err := u.deps.EventRepo.SendEmailPasswordChanged(ctx, user.Email); err != nil {
u.deps.Logger.Error().Err(err).Msg("error occured on sending email")
}
return nil
}
func (u *userService) getUserById(ctx context.Context, userId string) (*models.UserDTO, error) {

View File

@ -41,7 +41,7 @@ func NewRequestLogMiddleware(logger log.Logger, tracer trace.Tracer, prometheus
ctxLogger := logger.WithContext(c)
msg := fmt.Sprintf("Request %s %s %d %v", method, path, statusCode, latency)
msg := fmt.Sprintf("%s %s %d %v", method, path, statusCode, latency)
if statusCode >= 200 && statusCode < 400 {
ctxLogger.Log().Msg(msg)