diff --git a/.env b/.env index ad27df8..72eba97 100644 --- a/.env +++ b/.env @@ -1,22 +1,21 @@ # General application settings APP_NAME=codeword -HTTP_HOST=localhost -HTTP_PORT=8000 +HTTP_HOST="localhost" +HTTP_PORT="8000" # MongoDB settings -MONGO_HOST=127.0.0.1 -MONGO_PORT=27020 -MONGO_USER=test -MONGO_PASSWORD=test -MONGO_DB=admin -MONGO_AUTH=admin +MONGO_HOST="127.0.0.1" +MONGO_PORT="27020" +MONGO_USER="test" +MONGO_PASSWORD="test" +MONGO_DB="admin" +MONGO_AUTH="admin" # Redis settings REDIS_ADDR="localhost:6379" REDIS_PASS="admin" REDIS_DB=2 - # Keys PUBLIC_CURVE_KEY="-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyt4XuLovUY7i12K2PIMbQZOKn+wFFKUvxvKQDel049/+VMpHMx1FLolUKuyGp9zi6gOwjHsBPgc9oqr/eaXGQSh7Ult7i9f+Ht563Y0er5UU9Zc5ZPSxf9O75KYD48ruGkqiFoncDqPENK4dtUa7w0OqlN4bwVBbmIsP8B3EDC5Dof+vtiNTSHSXPx+zifKeZGyknp+nyOHVrRDhPjOhzQzCom0MSZA/sJYmps8QZgiPA0k4Z6jTupDymPOIwYeD2C57zSxnAv0AfC3/pZYJbZYH/0TszRzmy052DME3zMnhMK0ikdN4nzYqU0dkkA5kb5GtKDymspHIJ9eWbUuwgtg8Rq/LrVBj1I3UFgs0ibio40k6gqinLKslc5Y1I5mro7J3OSEP5eO/XeDLOLlOJjEqkrx4fviI1cL3m5L6QV905xmcoNZG1+RmOg7D7cZQUf27TXqM381jkbNdktm1JLTcMScxuo3vaRftnIVw70V8P8sIkaKY8S8HU1sQgE2LB9t04oog5u59htx2FHv4B13NEm8tt8Tv1PexpB4UVh7PIualF6SxdFBrKbraYej72wgjXVPQ0eGXtGGD57j8DUEzk7DK2OvIWhehlVqtiRnFdAvdBj2ynHT2/5FJ/Zpd4n5dKGJcQvy1U1qWMs+8M7AHfWyt2+nZ04s48+bK3yMCAwEAAQ==\n-----END PUBLIC KEY-----" @@ -24,3 +23,11 @@ PRIVATE_CURVE_KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIKn0BKwF3vZv SIGN_SECRET=group +# SMTP settings +SMTP_API_URL="https://api.smtp.bz/v1/smtp/send" +SMTP_HOST="connect.mailclient.bz" +SMTP_PORT="587" +SMTP_UNAME="kotilion.95@gmail.com" +SMTP_PASS="vWwbCSg4bf0p" +SMTP_API_KEY="P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev" +SMTP_SENDER="noreply@mailing.pena.digital" diff --git a/deployment/local/docker-compose.yaml b/deployment/local/docker-compose.yaml index 063ae2c..249e549 100644 --- a/deployment/local/docker-compose.yaml +++ b/deployment/local/docker-compose.yaml @@ -4,7 +4,7 @@ services: mongo: image: mongo ports: - - "${MONGO_PORT}:27017" + - "27020:27017" environment: - MONGO_INITDB_ROOT_USERNAME=${MONGO_USER} - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} diff --git a/internal/adapters/client/mail.go b/internal/adapters/client/mail.go index 4369a97..66ae333 100644 --- a/internal/adapters/client/mail.go +++ b/internal/adapters/client/mail.go @@ -1,36 +1,87 @@ package client import ( + "bytes" "fmt" - "net/smtp" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" + "mime/multipart" ) -type RecoveryEmailSender struct { - SmtpHost string - SmtpPort string - Username string - Password string - ApiKey string +type RecoveryEmailSenderDeps struct { + SmtpApiUrl string + SmtpHost string + SmtpPort string + SmtpSender string + Username string + Password string + ApiKey string + FiberClient *fiber.Client + Logger *zap.Logger } -// SendRecoveryEmail отправляет email с подписью для восстановления доступа -func (r *RecoveryEmailSender) SendRecoveryEmail(email, signature string) error { - // прост как пример пока что +type RecoveryEmailSender struct { + deps RecoveryEmailSenderDeps +} + +func NewRecoveryEmailSender(deps RecoveryEmailSenderDeps) *RecoveryEmailSender { + if deps.FiberClient == nil { + deps.FiberClient = fiber.AcquireClient() + } + return &RecoveryEmailSender{ + deps: deps, + } +} + +func (r *RecoveryEmailSender) SendRecoveryEmail(email string, signature []byte) error { + url := r.deps.SmtpApiUrl + + fmt.Println(email, signature) + message := fmt.Sprintf("To: %s\r\n"+ "Subject: Восстановление доступа\r\n"+ "\r\n"+ "Чтобы восстановить доступ, пожалуйста, перейдите по ссылке ниже:\r\n"+ " https://hub.pena.digital/codeword/restore/%s\r\n", email, signature) - auth := smtp.PlainAuth("", r.Username, r.Password, r.SmtpHost) + form := new(bytes.Buffer) + writer := multipart.NewWriter(form) + defer writer.Close() - err := smtp.SendMail(r.SmtpHost+":"+r.SmtpPort, auth, r.Username, []string{email}, []byte(message)) - if err != nil { - fmt.Printf("Ошибка при отправке письма: %s\n", err) + fields := map[string]string{ + "from": r.deps.SmtpSender, + "to": "pashamullin202@gmail.com", + "subject": "Восстановление доступа", + "html": message, + } + + for key, value := range fields { + if err := writer.WriteField(key, value); err != nil { + return err + } + } + + if err := writer.Close(); err != nil { return err } - fmt.Printf("Письмо для восстановления доступа отправлено на: %s\n", email) + req := r.deps.FiberClient.Post(url).Body(form.Bytes()).ContentType(writer.FormDataContentType()) + if r.deps.ApiKey != "" { + req.Set("Authorization", r.deps.ApiKey) + } + statusCode, body, errs := req.Bytes() + if errs != nil { + r.deps.Logger.Error("Ошибка при отправке запроса", zap.Error(errs[0])) + return errs[0] + } + + if statusCode != fiber.StatusOK { + err := fmt.Errorf("SMTP сервис вернул ошибку: %s Тело ответа: %s", statusCode, body) + r.deps.Logger.Error("Ошибка при отправке электронной почты", zap.Error(err)) + return err + } + + r.deps.Logger.Info("Письмо для восстановления отправлено", zap.String("email", email)) return nil } diff --git a/internal/app/app.go b/internal/app/app.go index cd1c38b..49cc0cc 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,13 +1,16 @@ package app import ( + "codeword/internal/adapters/client" controller "codeword/internal/controller/recovery" "codeword/internal/initialize" "codeword/internal/repository" httpserver "codeword/internal/server/http" "codeword/internal/services" "codeword/internal/utils/encrypt" + "codeword/internal/worker/recovery_worker" "context" + "github.com/gofiber/fiber/v2" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" "time" @@ -33,7 +36,17 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error { codewordRepo := repository.NewCodewordRepository(repository.Deps{Rdb: rdb, Mdb: mdb.Collection("codeword")}) userRepo := repository.NewUserRepository(repository.Deps{Rdb: nil, Mdb: mdb.Collection("users")}) - //recoveryEmailSender := &client.RecoveryEmailSender{} + recoveryEmailSender := client.NewRecoveryEmailSender(client.RecoveryEmailSenderDeps{ + SmtpApiUrl: cfg.SmtpApiUrl, + SmtpHost: cfg.SmtpHost, + SmtpPort: cfg.SmtpPort, + SmtpSender: cfg.SmtpSender, + Username: cfg.SmtpUsername, + Password: cfg.SmtpPassword, + ApiKey: cfg.SmtpApiKey, + FiberClient: &fiber.Client{}, + Logger: logger, + }) recoveryService := services.NewRecoveryService(services.Deps{ Logger: logger, @@ -44,6 +57,14 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error { recoveryController := controller.NewRecoveryController(logger, recoveryService) + recoveryWC := recovery_worker.NewRecoveryWC(recovery_worker.Deps{ + Logger: logger, + Redis: rdb, + EmailSender: recoveryEmailSender, + }) + + go recoveryWC.Start(ctx) + server := httpserver.NewServer(httpserver.ServerConfig{ Logger: logger, RecoveryController: recoveryController, diff --git a/internal/initialize/config.go b/internal/initialize/config.go index 1c98bec..e85c719 100644 --- a/internal/initialize/config.go +++ b/internal/initialize/config.go @@ -22,6 +22,13 @@ type Config struct { RedisAddr string `env:"REDIS_ADDR" envDefault:"localhost:6379"` RedisPassword string `env:"REDIS_PASS" envDefault:"admin"` RedisDB int `env:"REDIS_DB" envDefault:"2"` + SmtpApiUrl string `env:"SMTP_API_URL"` + SmtpHost string `env:"SMTP_HOST"` + SmtpPort string `env:"SMTP_PORT"` + SmtpUsername string `env:"SMTP_UNAME"` + SmtpPassword string `env:"SMTP_PASS"` + SmtpApiKey string `env:"SMTP_API_KEY"` + SmtpSender string `env:"SMTP_SENDER"` } func LoadConfig() (*Config, error) { diff --git a/internal/models/user.go b/internal/models/user.go index 1650e1e..544e227 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -30,6 +30,6 @@ type RestoreRequest struct { type RecoveryRecord struct { UserID string `bson:"user_id"` Email string `bson:"email"` - Key string `bson:"key"` + Key []byte `bson:"key"` CreatedAt time.Time `bson:"created_at"` } diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index e67d452..7c9a146 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -4,8 +4,8 @@ import ( "codeword/internal/models" "context" "encoding/json" - "fmt" "github.com/go-redis/redis/v8" + "github.com/pioz/faker" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/readpref" @@ -53,7 +53,7 @@ func (r *codewordRepository) StoreRecoveryRecord(ctx context.Context, userID str record := models.RecoveryRecord{ UserID: userID, Email: email, - Key: string(key), + Key: key, CreatedAt: time.Now(), } @@ -66,22 +66,24 @@ func (r *codewordRepository) StoreRecoveryRecord(ctx context.Context, userID str } func (r *codewordRepository) InsertToQueue(ctx context.Context, userID string, email string, key []byte) error { - task := models.RecoveryRecord{ - UserID: userID, - Email: email, - Key: string(key), - CreatedAt: time.Now(), - } + // todo не забыть убрать потом этот цикл + for i := 0; i < 10; i++ { - taskBytes, err := json.Marshal(task) - if err != nil { - return err - } + task := models.RecoveryRecord{ + UserID: userID + faker.String(), + Email: email, + Key: key, + CreatedAt: time.Now(), + } - uniqKey := fmt.Sprintf("needRecovery:%d", time.Now().UnixNano()) + taskBytes, err := json.Marshal(task) + if err != nil { + return err + } - if err := r.rdb.Set(ctx, uniqKey, taskBytes, 0).Err(); err != nil { - return err + if err := r.rdb.LPush(ctx, "recoveryQueue", taskBytes).Err(); err != nil { + return err + } } return nil diff --git a/internal/worker/recovery_worker/recovery_worker.go b/internal/worker/recovery_worker/recovery_worker.go index a1a2be2..93f495a 100644 --- a/internal/worker/recovery_worker/recovery_worker.go +++ b/internal/worker/recovery_worker/recovery_worker.go @@ -1 +1,83 @@ package recovery_worker + +import ( + "codeword/internal/adapters/client" + "codeword/internal/models" + "context" + "encoding/json" + "github.com/go-redis/redis/v8" + "go.uber.org/zap" + "time" +) + +type Deps struct { + Logger *zap.Logger + Redis *redis.Client + EmailSender *client.RecoveryEmailSender +} + +type recoveryWorker struct { + logger *zap.Logger + redis *redis.Client + emailSender *client.RecoveryEmailSender +} + +func NewRecoveryWC(deps Deps) *recoveryWorker { + return &recoveryWorker{ + logger: deps.Logger, + redis: deps.Redis, + emailSender: deps.EmailSender, + } +} + +func (wc *recoveryWorker) Start(ctx context.Context) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + wc.processTasks(ctx) + + case <-ctx.Done(): + return + } + } +} + +func (wc *recoveryWorker) processTasks(ctx context.Context) { + result, err := wc.redis.BRPop(ctx, 1*time.Second, "recoveryQueue").Result() + if err != nil { + if err != redis.Nil { + wc.logger.Error("Failed to BRPop from the recovery queue", zap.Error(err)) + } + return + } + + if len(result) < 2 { + wc.logger.Error("Received unexpected number of elements from BRPop", zap.Strings("result", result)) + return + } + + var task models.RecoveryRecord + if err = json.Unmarshal([]byte(result[1]), &task); err != nil { + wc.logger.Error("Failed to unmarshal recovery task", zap.String("key", result[0]), zap.Error(err)) + return + } + + err = wc.sendRecoveryTask(task) + if err != nil { + wc.logger.Error("Failed to send recovery task", zap.String("key", result[0]), zap.Error(err)) + return + } +} + +func (wc *recoveryWorker) sendRecoveryTask(task models.RecoveryRecord) error { + err := wc.emailSender.SendRecoveryEmail(task.Email, task.Key) + if err != nil { + wc.logger.Error("Failed to send recovery email", zap.Error(err)) + return err + } + wc.logger.Info("Recovery email sent successfully", zap.String("email", task.Email)) + return nil +}