diff --git a/.env b/.env index 4cdb4a2..19032cb 100644 --- a/.env +++ b/.env @@ -17,7 +17,7 @@ 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-----" +PUBLIC_CURVE_KEY="-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAEbnIvjIMle4rqVol6K2XUqOxHy1KJoNoZdKJrRUPKL4=\n-----END PUBLIC KEY-----" PRIVATE_CURVE_KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIKn0BKwF3vZvODgWAnUIwQhd8de5oZhY48gc23EWfrfs\n-----END PRIVATE KEY-----" diff --git a/internal/adapters/client/mail.go b/internal/adapters/client/mail.go index 361f392..a3836cd 100644 --- a/internal/adapters/client/mail.go +++ b/internal/adapters/client/mail.go @@ -10,15 +10,17 @@ import ( ) type RecoveryEmailSenderDeps struct { - SmtpApiUrl string - SmtpHost string - SmtpPort string - SmtpSender string - Username string - Password string - ApiKey string - FiberClient *fiber.Client - Logger *zap.Logger + SmtpApiUrl string + SmtpHost string + SmtpPort string + SmtpSender string + Username string + Password string + ApiKey string + FiberClient *fiber.Client + Logger *zap.Logger + CodewordHost string + CodewordPort string } type RecoveryEmailSender struct { @@ -40,7 +42,7 @@ func (r *RecoveryEmailSender) SendRecoveryEmail(email string, signature []byte) fmt.Println(email, signatureStr) - message := fmt.Sprintf("https://hub.pena.digital/codeword/restore/%s", signatureStr) + message := fmt.Sprintf("http://"+r.deps.CodewordHost+":"+r.deps.CodewordPort+"/recover/%s", signatureStr) form := new(bytes.Buffer) writer := multipart.NewWriter(form) @@ -70,16 +72,16 @@ func (r *RecoveryEmailSender) SendRecoveryEmail(email string, signature []byte) statusCode, body, errs := req.Bytes() if errs != nil { - r.deps.Logger.Error("Ошибка при отправке запроса", zap.Error(errs[0])) + r.deps.Logger.Error("Error sending request", zap.Error(errs[0])) return errs[0] } if statusCode != fiber.StatusOK { - err := fmt.Errorf("SMTP сервис вернул ошибку: %s Тело ответа: %s", statusCode, body) + err := fmt.Errorf("the SMTP service returned an error: %s Response body: %s", statusCode, body) r.deps.Logger.Error("Ошибка при отправке электронной почты", zap.Error(err)) return err } - r.deps.Logger.Info("Письмо для восстановления отправлено", zap.String("email", email)) + //r.deps.Logger.Info("Recovery email sent", zap.String("email", email)) return nil } diff --git a/internal/app/app.go b/internal/app/app.go index e8244c0..1f40308 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,16 +1,13 @@ 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" @@ -26,33 +23,16 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error { } rdb, err := initialize.InitializeRedis(ctx, cfg) - - encryptService := encrypt.New(&encrypt.EncryptDeps{ - PublicKey: cfg.PublicCurveKey, - PrivateKey: cfg.PrivateCurveKey, - SignSecret: cfg.SignSecret, - }) - + encrypt := initialize.InitializeEncrypt(cfg) codewordRepo := repository.NewCodewordRepository(repository.Deps{Rdb: rdb, Mdb: mdb.Collection("codeword")}) userRepo := repository.NewUserRepository(repository.Deps{Rdb: nil, Mdb: mdb.Collection("users")}) - - 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, - }) + recoveryEmailSender := initialize.InitializeRecoveryEmailSender(cfg, logger) recoveryService := services.NewRecoveryService(services.Deps{ Logger: logger, CodewordRepository: codewordRepo, UserRepository: userRepo, - EncryptService: encryptService, + Encrypt: encrypt, }) recoveryController := controller.NewRecoveryController(logger, recoveryService, cfg.DefaultRedirectionURL) diff --git a/internal/controller/recovery/recovery_controller.go b/internal/controller/recovery/recovery_controller.go index 2fa0761..bf16b61 100644 --- a/internal/controller/recovery/recovery_controller.go +++ b/internal/controller/recovery/recovery_controller.go @@ -2,6 +2,7 @@ package controller import ( "codeword/internal/services" + "encoding/base64" "github.com/gofiber/fiber/v2" "go.uber.org/zap" "time" @@ -50,9 +51,10 @@ func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) } - //sign := referralURL + string(key) + signUrl := redirectionURL + base64.URLEncoding.EncodeToString(key) + sign := base64.URLEncoding.EncodeToString(key) - id, err := r.service.StoreRecoveryRecord(c.Context(), user.ID.Hex(), user.Email, key) + id, err := r.service.StoreRecoveryRecord(c.Context(), user.ID.Hex(), user.Email, sign, signUrl) if err != nil { r.logger.Error("Failed to store recovery record", zap.Error(err)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) @@ -72,14 +74,13 @@ func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { // HandleRecoveryLink обрабатывает ссылку восстановления и обменивает ее на токены func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error { key := c.Params("sign") - // тут получается - record, err := r.service.GetRecoveryRecord(c.Context(), []byte(key)) + + record, err := r.service.GetRecoveryRecord(c.Context(), key) if err != nil { r.logger.Error("Failed to get recovery record", zap.Error(err)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } - // проверка на более чем 15 минут if time.Since(record.CreatedAt) > 15*time.Minute { r.logger.Error("Recovery link expired", zap.String("signature", key)) return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Recovery link expired"}) diff --git a/internal/initialize/encrypt.go b/internal/initialize/encrypt.go new file mode 100644 index 0000000..abb49ed --- /dev/null +++ b/internal/initialize/encrypt.go @@ -0,0 +1,13 @@ +package initialize + +import ( + "codeword/internal/utils/encrypt" +) + +func InitializeEncrypt(cfg Config) *encrypt.Encrypt { + return encrypt.New(&encrypt.EncryptDeps{ + PublicKey: cfg.PublicCurveKey, + PrivateKey: cfg.PrivateCurveKey, + SignSecret: cfg.SignSecret, + }) +} diff --git a/internal/initialize/mail.go b/internal/initialize/mail.go new file mode 100644 index 0000000..dcdf3c0 --- /dev/null +++ b/internal/initialize/mail.go @@ -0,0 +1,23 @@ +package initialize + +import ( + "codeword/internal/adapters/client" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +func InitializeRecoveryEmailSender(cfg Config, logger *zap.Logger) *client.RecoveryEmailSender { + return 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, + CodewordHost: cfg.HTTPHost, + CodewordPort: cfg.HTTPPort, + }) +} diff --git a/internal/models/user.go b/internal/models/user.go index aff2f7d..899b51d 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -20,7 +20,8 @@ type User struct { type RestoreRequest struct { ID primitive.ObjectID `bson:"_id,omitempty"` CreatedAt time.Time `bson:"created_at,omitempty"` - Sign []byte `bson:"sign,omitempty"` + Sign string `bson:"sign,omitempty"` + SignUrl string `bson:"sign_url,omitempty"` Email string `bson:"email,omitempty"` UserID string `bson:"user_id,omitempty"` Sent bool `bson:"sent"` diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index 311be86..d93720d 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "github.com/go-redis/redis/v8" - "github.com/pioz/faker" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" @@ -50,13 +49,14 @@ func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models return &user, nil } -func (r *codewordRepository) StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) (string, error) { +func (r *codewordRepository) StoreRecoveryRecord(ctx context.Context, userID, email, key, url string) (string, error) { newID := primitive.NewObjectID() record := models.RestoreRequest{ ID: newID, UserID: userID, Email: email, Sign: key, + SignUrl: url, CreatedAt: time.Now(), } @@ -69,31 +69,36 @@ func (r *codewordRepository) StoreRecoveryRecord(ctx context.Context, userID str } func (r *codewordRepository) InsertToQueue(ctx context.Context, userID string, email string, key []byte, id string) error { - // todo не забыть убрать потом этот цикл - for i := 0; i < 10; i++ { + task := models.RecoveryRecord{ + ID: id, + UserID: userID, + Email: email, + Key: key, + } - task := models.RecoveryRecord{ - ID: id, - UserID: userID + faker.String(), - Email: email, - Key: key, - } + taskBytes, err := json.Marshal(task) + if err != nil { + return err + } - taskBytes, err := json.Marshal(task) - if err != nil { - return err - } - - if err := r.rdb.LPush(ctx, "recoveryQueue", taskBytes).Err(); err != nil { - return err - } + if err := r.rdb.LPush(ctx, "recoveryQueue", taskBytes).Err(); err != nil { + return err } return nil } -func (r *codewordRepository) GetRecoveryRecord(ctx context.Context, key []byte) (*models.RestoreRequest, error) { - return &models.RestoreRequest{UserID: "123", Sign: key, CreatedAt: time.Now()}, nil +func (r *codewordRepository) GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) { + var restoreRequest models.RestoreRequest + + filter := bson.M{"sign": key} + + err := r.mdb.FindOne(ctx, filter).Decode(&restoreRequest) + if err != nil { + return nil, err + } + + return &restoreRequest, nil } func (r *codewordRepository) Ping(ctx context.Context) error { diff --git a/internal/services/recovery_service.go b/internal/services/recovery_service.go index 588801a..0bab162 100644 --- a/internal/services/recovery_service.go +++ b/internal/services/recovery_service.go @@ -4,14 +4,15 @@ import ( "codeword/internal/models" "codeword/internal/utils/encrypt" "context" + "encoding/base64" "go.uber.org/zap" ) type CodewordRepository interface { - StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) (string, error) + StoreRecoveryRecord(ctx context.Context, userID, email, key, signUrl string) (string, error) InsertToQueue(ctx context.Context, userID string, email string, key []byte, id string) error Ping(ctx context.Context) error - GetRecoveryRecord(ctx context.Context, key []byte) (*models.RestoreRequest, error) + GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) } type UserRepository interface { @@ -22,14 +23,14 @@ type Deps struct { Logger *zap.Logger CodewordRepository CodewordRepository UserRepository UserRepository - EncryptService *encrypt.Encrypt + Encrypt *encrypt.Encrypt } type RecoveryService struct { logger *zap.Logger repositoryCodeword CodewordRepository repositoryUser UserRepository - encryptService *encrypt.Encrypt + encrypt *encrypt.Encrypt } func NewRecoveryService(deps Deps) *RecoveryService { @@ -37,32 +38,48 @@ func NewRecoveryService(deps Deps) *RecoveryService { logger: deps.Logger, repositoryCodeword: deps.CodewordRepository, repositoryUser: deps.UserRepository, - encryptService: deps.EncryptService, + encrypt: deps.Encrypt, } } // GenerateKey генерирует ключ, используя шифрование на основе эллиптической кривой func (s *RecoveryService) GenerateKey() ([]byte, error) { - key, err := s.encryptService.SignCommonSecret() + key, err := s.encrypt.SignCommonSecret() if err != nil { + s.logger.Error("Failed to generate unique key for user", zap.Error(err)) return nil, err } return key, nil } func (s *RecoveryService) Ping(ctx context.Context) error { - return s.repositoryCodeword.Ping(ctx) + err := s.repositoryCodeword.Ping(ctx) + if err != nil { + s.logger.Error("Failed to ping database", zap.Error(err)) + return err + } + return nil } // FindUserByEmail ищет пользователя по электронной почте func (s *RecoveryService) FindUserByEmail(ctx context.Context, email string) (*models.User, error) { - return s.repositoryUser.FindByEmail(ctx, email) + user, err := s.repositoryUser.FindByEmail(ctx, email) + if err != nil { + s.logger.Error("Failed to find user by email", zap.String("email", email), zap.Error(err)) + return nil, err + } + if user == nil { + s.logger.Info("No user found with email", zap.String("email", email)) + return nil, nil + } + return user, nil } // StoreRecoveryRecord сохраняет запись восстановления в базе данных -func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) (string, error) { - id, err := s.repositoryCodeword.StoreRecoveryRecord(ctx, userID, email, key) +func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, userID, email, key, signUrl string) (string, error) { + id, err := s.repositoryCodeword.StoreRecoveryRecord(ctx, userID, email, key, signUrl) if err != nil { + s.logger.Error("Failed save data in mongoDB for email", zap.String("email", email), zap.Error(err)) return "", err } return id, nil @@ -70,12 +87,37 @@ func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, userID string // SendRecoveryEmail посылает письмо для восстановления доступа пользователю func (s *RecoveryService) RecoveryEmailTask(ctx context.Context, userID string, email string, key []byte, id string) error { - return s.repositoryCodeword.InsertToQueue(ctx, userID, email, key, id) + err := s.repositoryCodeword.InsertToQueue(ctx, userID, email, key, id) + if err != nil { + s.logger.Error("Failed creating a task to send a worker by email", zap.String("email", email), zap.Error(err)) + return err + } + return nil } // GetRecoveryRecord получает запись восстановления из базы данных -func (s *RecoveryService) GetRecoveryRecord(ctx context.Context, key []byte) (*models.RestoreRequest, error) { - return s.repositoryCodeword.GetRecoveryRecord(ctx, key) +func (s *RecoveryService) GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) { + byteKey, err := base64.URLEncoding.DecodeString(key) + if err != nil { + s.logger.Error("Failed to decode string signature to []byte format", zap.String("signature", key), zap.Error(err)) + return nil, err + } + + result, err := s.encrypt.VerifySignature(byteKey) + if err != nil { + s.logger.Error("Failed to verify signature", zap.String("signature", key), zap.Error(err)) + return nil, err + } + + if result { + req, err := s.repositoryCodeword.GetRecoveryRecord(ctx, key) + if err != nil { + s.logger.Error("Failed to obtain signature recovery data", zap.String("signature", key), zap.Error(err)) + return nil, err + } + return req, nil + } + return nil, nil } // ExchangeForTokens обменивает ссылку восстановления на токены используя сервис аутентификации. diff --git a/internal/utils/encrypt/encrypt_util.go b/internal/utils/encrypt/encrypt_util.go index 36695e3..405c5e4 100644 --- a/internal/utils/encrypt/encrypt_util.go +++ b/internal/utils/encrypt/encrypt_util.go @@ -4,6 +4,7 @@ import ( "crypto/ed25519" "crypto/x509" "encoding/pem" + "errors" "fmt" ) @@ -46,7 +47,7 @@ func (receiver *Encrypt) VerifySignature(signature []byte) (isValid bool, err er publicKey, ok := rawPublicKey.(ed25519.PublicKey) if !ok { - return false, fmt.Errorf("failed convert to ed25519.PrivateKey on of : %w", err) + return false, errors.New("public key is not of type ed25519.PublicKey") } return ed25519.Verify(publicKey, []byte(receiver.signSecret), signature), nil diff --git a/internal/worker/recovery_worker/recovery_worker.go b/internal/worker/recovery_worker/recovery_worker.go index 6efe707..79f73f0 100644 --- a/internal/worker/recovery_worker/recovery_worker.go +++ b/internal/worker/recovery_worker/recovery_worker.go @@ -5,7 +5,6 @@ import ( "codeword/internal/models" "context" "encoding/json" - "fmt" "github.com/go-redis/redis/v8" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -80,7 +79,6 @@ func (wc *recoveryWorker) processTasks(ctx context.Context) { } func (wc *recoveryWorker) sendRecoveryTask(ctx context.Context, task models.RecoveryRecord) error { - fmt.Println("task.Key", task.Key) err := wc.emailSender.SendRecoveryEmail(task.Email, task.Key) if err != nil { wc.logger.Error("Failed to send recovery email", zap.Error(err)) @@ -113,6 +111,6 @@ func (wc *recoveryWorker) sendRecoveryTask(ctx context.Context, task models.Reco zap.String("ID", task.ID)) } - wc.logger.Info("Recovery email sent and restore request updated successfully", zap.String("email", task.Email)) + //wc.logger.Info("Recovery email sent and restore request updated successfully", zap.String("email", task.Email)) return nil } diff --git a/tests/repository_test/repository_test.go b/tests/repository_test/repository_test.go index ffb03a1..d12754c 100644 --- a/tests/repository_test/repository_test.go +++ b/tests/repository_test/repository_test.go @@ -76,9 +76,9 @@ func TestStoreRecoveryRecord(t *testing.T) { for i := 0; i < 10; i++ { userID := faker.String() email := faker.Email() - key := []byte("test_recovery_key") + key := "test_recovery_key" - id, err := userRepo.StoreRecoveryRecord(ctx, userID, email, key) + id, err := userRepo.StoreRecoveryRecord(ctx, userID, email, key, "def.url") assert.NoError(t, err) fmt.Println(id)