diff --git a/cmd/backend/server/handlers/user_send_verify.go b/cmd/backend/server/handlers/user_send_verify.go index cb0bec1..7938d4c 100644 --- a/cmd/backend/server/handlers/user_send_verify.go +++ b/cmd/backend/server/handlers/user_send_verify.go @@ -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 }, ) diff --git a/cmd/coworker/config.go b/cmd/coworker/config.go new file mode 100644 index 0000000..e40f597 --- /dev/null +++ b/cmd/coworker/config.go @@ -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"` +} diff --git a/cmd/coworker/emailer.go b/cmd/coworker/emailer.go new file mode 100644 index 0000000..7c496c6 --- /dev/null +++ b/cmd/coworker/emailer.go @@ -0,0 +1,87 @@ +package main + +import ( + "html/template" + "strings" + + "gopkg.in/gomail.v2" +) + +const MSG_TEXT = ` + + + + +

{{.Text}}

+ {{if .Link}} + Clicklink

+ {{end}} + + +` + +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) +} diff --git a/cmd/coworker/main.go b/cmd/coworker/main.go index bf06f13..147d215 100644 --- a/cmd/coworker/main.go +++ b/cmd/coworker/main.go @@ -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 = ` - - - - -

This message was sent because you forgot a password

-

To change a password, use thislink

- - -` - -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") +} diff --git a/internal/core/repos/event_repo.go b/internal/core/repos/event_repo.go index abba12e..ac3e588 100644 --- a/internal/core/repos/event_repo.go +++ b/internal/core/repos/event_repo.go @@ -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) } diff --git a/internal/core/services/user_service.go b/internal/core/services/user_service.go index 2d91add..6a47cf3 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -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) { diff --git a/internal/http_server/request_log.go b/internal/http_server/request_log.go index 0a32ec3..f3c7e5b 100644 --- a/internal/http_server/request_log.go +++ b/internal/http_server/request_log.go @@ -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)