This commit is contained in:
Pavel 2024-01-04 14:27:50 +03:00
parent cfc90597cc
commit d944d14ec2
12 changed files with 148 additions and 82 deletions

2
.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-----"

@ -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
}

@ -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)

@ -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"})

@ -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,
})
}

@ -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,
})
}

@ -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"`

@ -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 {

@ -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 обменивает ссылку восстановления на токены используя сервис аутентификации.

@ -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 <VerifySignature> of <EncryptService>: %w", err)
return false, errors.New("public key is not of type ed25519.PublicKey")
}
return ed25519.Verify(publicKey, []byte(receiver.signSecret), signature), nil

@ -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
}

@ -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)