diff --git a/.env b/.env index 72eba97..4cdb4a2 100644 --- a/.env +++ b/.env @@ -31,3 +31,6 @@ SMTP_UNAME="kotilion.95@gmail.com" SMTP_PASS="vWwbCSg4bf0p" SMTP_API_KEY="P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev" SMTP_SENDER="noreply@mailing.pena.digital" + +# URL settings +DEFAULT_REDIRECTION_URL = "def.url" \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go index 49cc0cc..e8244c0 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -55,12 +55,13 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error { EncryptService: encryptService, }) - recoveryController := controller.NewRecoveryController(logger, recoveryService) + recoveryController := controller.NewRecoveryController(logger, recoveryService, cfg.DefaultRedirectionURL) recoveryWC := recovery_worker.NewRecoveryWC(recovery_worker.Deps{ Logger: logger, Redis: rdb, EmailSender: recoveryEmailSender, + Mongo: mdb.Collection("codeword"), }) go recoveryWC.Start(ctx) diff --git a/internal/controller/recovery/recovery_controller.go b/internal/controller/recovery/recovery_controller.go index e92d318..2fa0761 100644 --- a/internal/controller/recovery/recovery_controller.go +++ b/internal/controller/recovery/recovery_controller.go @@ -8,14 +8,16 @@ import ( ) type RecoveryController struct { - logger *zap.Logger - service *services.RecoveryService + logger *zap.Logger + service *services.RecoveryService + defaultURL string } -func NewRecoveryController(logger *zap.Logger, service *services.RecoveryService) *RecoveryController { +func NewRecoveryController(logger *zap.Logger, service *services.RecoveryService, defaultRedirectionURL string) *RecoveryController { return &RecoveryController{ - logger: logger, - service: service, + logger: logger, + service: service, + defaultURL: defaultRedirectionURL, } } @@ -23,9 +25,18 @@ func (r *RecoveryController) HandlePingDB(c *fiber.Ctx) error { return r.service.Ping(c.Context()) } +// TODO add deps struct, counnt params >3 // HandleRecoveryRequest обрабатывает запрос на восстановление пароля func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { email := c.FormValue("email") + referralURL := c.Get("Referrer") + redirectionURL := c.FormValue("RedirectionURL") + + if redirectionURL == "" && referralURL != "" { + redirectionURL = referralURL + } else if redirectionURL == "" { + redirectionURL = r.defaultURL + } key, err := r.service.GenerateKey() if err != nil { @@ -39,26 +50,30 @@ func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) } - err = r.service.StoreRecoveryRecord(c.Context(), user.ID.Hex(), user.Email, key) + //sign := referralURL + string(key) + + id, err := r.service.StoreRecoveryRecord(c.Context(), user.ID.Hex(), user.Email, key) 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"}) } - err = r.service.RecoveryEmailTask(c.Context(), user.ID.Hex(), email, key) + err = r.service.RecoveryEmailTask(c.Context(), user.ID.Hex(), email, key, id) if err != nil { r.logger.Error("Failed to send recovery email", zap.Error(err)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } - return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Recovery email sent successfully"}) + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "id": id, + }) } // HandleRecoveryLink обрабатывает ссылку восстановления и обменивает ее на токены func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error { key := c.Params("sign") // тут получается - record, err := r.service.GetRecoveryRecord(c.Context(), key) + record, err := r.service.GetRecoveryRecord(c.Context(), []byte(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"}) diff --git a/internal/initialize/config.go b/internal/initialize/config.go index e85c719..b51bc54 100644 --- a/internal/initialize/config.go +++ b/internal/initialize/config.go @@ -7,28 +7,29 @@ import ( ) type Config struct { - AppName string `env:"APP_NAME" envDefault:"codeword"` - HTTPHost string `env:"HTTP_HOST" envDefault:"localhost"` - HTTPPort string `env:"HTTP_PORT" envDefault:"3000"` - MongoHost string `env:"MONGO_HOST" envDefault:"127.0.0.1"` - MongoPort string `env:"MONGO_PORT" envDefault:"27020"` - MongoUser string `env:"MONGO_USER" envDefault:"test"` - MongoPassword string `env:"MONGO_PASSWORD" envDefault:"test"` - MongoDatabase string `env:"MONGO_DB" envDefault:"admin"` - MongoAuth string `env:"MONGO_AUTH" envDefault:"admin"` - PublicCurveKey string `env:"PUBLIC_CURVE_KEY"` - PrivateCurveKey string `env:"PRIVATE_CURVE_KEY"` - SignSecret string `env:"SIGN_SECRET"` - 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"` + AppName string `env:"APP_NAME" envDefault:"codeword"` + HTTPHost string `env:"HTTP_HOST" envDefault:"localhost"` + HTTPPort string `env:"HTTP_PORT" envDefault:"3000"` + MongoHost string `env:"MONGO_HOST" envDefault:"127.0.0.1"` + MongoPort string `env:"MONGO_PORT" envDefault:"27020"` + MongoUser string `env:"MONGO_USER" envDefault:"test"` + MongoPassword string `env:"MONGO_PASSWORD" envDefault:"test"` + MongoDatabase string `env:"MONGO_DB" envDefault:"admin"` + MongoAuth string `env:"MONGO_AUTH" envDefault:"admin"` + PublicCurveKey string `env:"PUBLIC_CURVE_KEY"` + PrivateCurveKey string `env:"PRIVATE_CURVE_KEY"` + SignSecret string `env:"SIGN_SECRET"` + 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"` + DefaultRedirectionURL string `env:"DEFAULT_REDIRECTION_URL"` } func LoadConfig() (*Config, error) { diff --git a/internal/models/user.go b/internal/models/user.go index 544e227..aff2f7d 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -18,18 +18,18 @@ type User struct { } type RestoreRequest struct { - ID string // xid или ObjectID - CreatedAt time.Time - Sign string // подпись - Email string // email из запроса - UserID string // айдишник юзера, которого нашли по email - Sent bool - SentAt time.Time + ID primitive.ObjectID `bson:"_id,omitempty"` + CreatedAt time.Time `bson:"created_at,omitempty"` + Sign []byte `bson:"sign,omitempty"` + Email string `bson:"email,omitempty"` + UserID string `bson:"user_id,omitempty"` + Sent bool `bson:"sent"` + SentAt time.Time `bson:"sent_at"` } type RecoveryRecord struct { - UserID string `bson:"user_id"` - Email string `bson:"email"` - Key []byte `bson:"key"` - CreatedAt time.Time `bson:"created_at"` + ID string + UserID string + Email string + Key []byte } diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index 7c9a146..311be86 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -7,6 +7,7 @@ import ( "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" "go.mongodb.org/mongo-driver/mongo/readpref" "time" @@ -49,31 +50,33 @@ 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) error { - record := models.RecoveryRecord{ +func (r *codewordRepository) StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) (string, error) { + newID := primitive.NewObjectID() + record := models.RestoreRequest{ + ID: newID, UserID: userID, Email: email, - Key: key, + Sign: key, CreatedAt: time.Now(), } _, err := r.mdb.InsertOne(ctx, record) if err != nil { - return err + return "", err } - return nil + return newID.Hex(), nil } -func (r *codewordRepository) InsertToQueue(ctx context.Context, userID string, email string, key []byte) error { +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{ - UserID: userID + faker.String(), - Email: email, - Key: key, - CreatedAt: time.Now(), + ID: id, + UserID: userID + faker.String(), + Email: email, + Key: key, } taskBytes, err := json.Marshal(task) @@ -89,7 +92,7 @@ func (r *codewordRepository) InsertToQueue(ctx context.Context, userID string, e return nil } -func (r *codewordRepository) GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) { +func (r *codewordRepository) GetRecoveryRecord(ctx context.Context, key []byte) (*models.RestoreRequest, error) { return &models.RestoreRequest{UserID: "123", Sign: key, CreatedAt: time.Now()}, nil } diff --git a/internal/services/recovery_service.go b/internal/services/recovery_service.go index 0d5ea87..588801a 100644 --- a/internal/services/recovery_service.go +++ b/internal/services/recovery_service.go @@ -8,10 +8,10 @@ import ( ) type CodewordRepository interface { - StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) error - InsertToQueue(ctx context.Context, userID string, email string, key []byte) error + StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) (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 string) (*models.RestoreRequest, error) + GetRecoveryRecord(ctx context.Context, key []byte) (*models.RestoreRequest, error) } type UserRepository interface { @@ -60,17 +60,21 @@ func (s *RecoveryService) FindUserByEmail(ctx context.Context, email string) (*m } // StoreRecoveryRecord сохраняет запись восстановления в базе данных -func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) error { - return s.repositoryCodeword.StoreRecoveryRecord(ctx, userID, email, key) +func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) (string, error) { + id, err := s.repositoryCodeword.StoreRecoveryRecord(ctx, userID, email, key) + if err != nil { + return "", err + } + return id, nil } // SendRecoveryEmail посылает письмо для восстановления доступа пользователю -func (s *RecoveryService) RecoveryEmailTask(ctx context.Context, userID string, email string, key []byte) error { - return s.repositoryCodeword.InsertToQueue(ctx, userID, email, key) +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) } // GetRecoveryRecord получает запись восстановления из базы данных -func (s *RecoveryService) GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) { +func (s *RecoveryService) GetRecoveryRecord(ctx context.Context, key []byte) (*models.RestoreRequest, error) { return s.repositoryCodeword.GetRecoveryRecord(ctx, key) } diff --git a/internal/worker/recovery_worker/recovery_worker.go b/internal/worker/recovery_worker/recovery_worker.go index 93f495a..f6f9c4f 100644 --- a/internal/worker/recovery_worker/recovery_worker.go +++ b/internal/worker/recovery_worker/recovery_worker.go @@ -6,6 +6,9 @@ import ( "context" "encoding/json" "github.com/go-redis/redis/v8" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" "time" ) @@ -14,12 +17,14 @@ type Deps struct { Logger *zap.Logger Redis *redis.Client EmailSender *client.RecoveryEmailSender + Mongo *mongo.Collection } type recoveryWorker struct { logger *zap.Logger redis *redis.Client emailSender *client.RecoveryEmailSender + mongo *mongo.Collection } func NewRecoveryWC(deps Deps) *recoveryWorker { @@ -27,6 +32,7 @@ func NewRecoveryWC(deps Deps) *recoveryWorker { logger: deps.Logger, redis: deps.Redis, emailSender: deps.EmailSender, + mongo: deps.Mongo, } } @@ -65,19 +71,46 @@ func (wc *recoveryWorker) processTasks(ctx context.Context) { return } - err = wc.sendRecoveryTask(task) + err = wc.sendRecoveryTask(ctx, 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 { +func (wc *recoveryWorker) sendRecoveryTask(ctx context.Context, 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)) + + update := bson.M{ + "$set": bson.M{ + "sent": true, + "sent_at": time.Now(), + }, + } + + objectID, err := primitive.ObjectIDFromHex(task.ID) + if err != nil { + wc.logger.Error("Invalid ObjectID", zap.String("ID", task.ID), zap.Error(err)) + return err + } + + filter := bson.M{"_id": objectID} + + result, err := wc.mongo.UpdateOne(ctx, filter, update) + if err != nil { + wc.logger.Error("Failed to update restore request", zap.Error(err)) + return err + } + + if result.ModifiedCount == 0 { + wc.logger.Warn("No documents were updated - this may indicate the document was not found", + zap.String("ID", task.ID)) + } + + 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 566dc99..ffb03a1 100644 --- a/tests/repository_test/repository_test.go +++ b/tests/repository_test/repository_test.go @@ -4,6 +4,7 @@ import ( "codeword/internal/models" "codeword/internal/repository" "context" + "fmt" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" @@ -77,14 +78,15 @@ func TestStoreRecoveryRecord(t *testing.T) { email := faker.Email() key := []byte("test_recovery_key") - err = userRepo.StoreRecoveryRecord(ctx, userID, email, key) + id, err := userRepo.StoreRecoveryRecord(ctx, userID, email, key) assert.NoError(t, err) + fmt.Println(id) - var storedRecord models.RecoveryRecord + var storedRecord models.RestoreRequest err = codeword.FindOne(ctx, bson.M{"user_id": userID}).Decode(&storedRecord) assert.NoError(t, err) assert.Equal(t, email, storedRecord.Email) - assert.Equal(t, string(key), storedRecord.Key) + assert.Equal(t, key, storedRecord.Sign) } _ = database.Drop(ctx)