diff --git a/go.mod b/go.mod index 96c034a..aec7e55 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,21 @@ module codeword go 1.21 +require ( + github.com/caarlos0/env/v8 v8.0.0 + github.com/gofiber/fiber/v2 v2.51.0 + github.com/stretchr/testify v1.8.1 + go.mongodb.org/mongo-driver v1.13.1 + go.uber.org/zap v1.26.0 +) + require ( github.com/andybalholm/brotli v1.0.5 // indirect - github.com/caarlos0/env/v8 v8.0.0 // indirect - github.com/gofiber/fiber/v2 v2.51.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-faker/faker/v4 v4.2.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/google/uuid v1.4.0 // indirect github.com/klauspost/compress v1.16.7 // indirect @@ -14,6 +24,8 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/pioz/faker v1.7.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.50.0 // indirect @@ -22,11 +34,10 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - go.mongodb.org/mongo-driver v1.13.1 // indirect go.uber.org/multierr v1.10.0 // indirect - go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.8.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d1d2a42..f8ff51c 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,22 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0= github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/go-faker/faker/v4 v4.2.0 h1:dGebOupKwssrODV51E0zbMrv5e2gO9VWSLNC1WDCpWg= +github.com/go-faker/faker/v4 v4.2.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol/dhcpRub4= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ= github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -24,8 +33,20 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/pioz/faker v1.7.3 h1:Tez8Emuq0UN+/d6mo3a9m/9ZZ/zdfJk0c5RtRatrceM= +github.com/pioz/faker v1.7.3/go.mod h1:xSpay5w/oz1a6+ww0M3vfpe40pSIykeUPeWEc3TvVlc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= @@ -43,6 +64,8 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= @@ -84,4 +107,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/adapters/client/mail.go b/internal/adapters/client/mail.go index ecf543b..4369a97 100644 --- a/internal/adapters/client/mail.go +++ b/internal/adapters/client/mail.go @@ -1,13 +1,36 @@ package client -import "fmt" +import ( + "fmt" + "net/smtp" +) -type RecoveryEmailSender struct{} +type RecoveryEmailSender struct { + SmtpHost string + SmtpPort string + Username string + Password string + ApiKey string +} // SendRecoveryEmail отправляет email с подписью для восстановления доступа func (r *RecoveryEmailSender) SendRecoveryEmail(email, signature string) error { + // прост как пример пока что + 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) - fmt.Printf("Отправляем письмо для восстановления доступа на: %s с подписью: %s\n", email, signature) + auth := smtp.PlainAuth("", r.Username, r.Password, r.SmtpHost) + + err := smtp.SendMail(r.SmtpHost+":"+r.SmtpPort, auth, r.Username, []string{email}, []byte(message)) + if err != nil { + fmt.Printf("Ошибка при отправке письма: %s\n", err) + return err + } + + fmt.Printf("Письмо для восстановления доступа отправлено на: %s\n", email) return nil } diff --git a/internal/app/app.go b/internal/app/app.go index 0abef64..cd1c38b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,7 +1,6 @@ package app import ( - "codeword/internal/adapters/client" controller "codeword/internal/controller/recovery" "codeword/internal/initialize" "codeword/internal/repository" @@ -9,7 +8,6 @@ import ( "codeword/internal/services" "codeword/internal/utils/encrypt" "context" - "fmt" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" "time" @@ -24,28 +22,30 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error { return err } + rdb, err := initialize.InitializeRedis(ctx, cfg) + encryptService := encrypt.New(&encrypt.EncryptDeps{ PublicKey: cfg.PublicCurveKey, PrivateKey: cfg.PrivateCurveKey, SignSecret: cfg.SignSecret, }) - userRepo := repository.NewUserRepository(mdb) + 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.RecoveryEmailSender{} recoveryService := services.NewRecoveryService(services.Deps{ - Logger: logger, - Repository: userRepo, - Email: recoveryEmailSender, - EncryptService: encryptService, + Logger: logger, + CodewordRepository: codewordRepo, + UserRepository: userRepo, + EncryptService: encryptService, }) recoveryController := controller.NewRecoveryController(logger, recoveryService) server := httpserver.NewServer(httpserver.ServerConfig{ Logger: logger, - Repo: userRepo, RecoveryController: recoveryController, }) @@ -57,8 +57,6 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error { <-ctx.Done() - fmt.Println("<-ctx.Done()") - if err := shutdownApp(server, mdb, logger); err != nil { return err } diff --git a/internal/controller/recovery/recovery_controller.go b/internal/controller/recovery/recovery_controller.go index d2a5330..e92d318 100644 --- a/internal/controller/recovery/recovery_controller.go +++ b/internal/controller/recovery/recovery_controller.go @@ -2,7 +2,6 @@ package controller import ( "codeword/internal/services" - "fmt" "github.com/gofiber/fiber/v2" "go.uber.org/zap" "time" @@ -20,6 +19,10 @@ func NewRecoveryController(logger *zap.Logger, service *services.RecoveryService } } +func (r *RecoveryController) HandlePingDB(c *fiber.Ctx) error { + return r.service.Ping(c.Context()) +} + // HandleRecoveryRequest обрабатывает запрос на восстановление пароля func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { email := c.FormValue("email") @@ -29,24 +32,20 @@ func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { r.logger.Error("Failed to generate key", zap.Error(err)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } - fmt.Println(key) - user, err := r.service.FindUserByEmail(email) - if err != nil { + user, err := r.service.FindUserByEmail(c.Context(), email) + if err != nil || user == nil { r.logger.Error("Failed to find user by email", zap.Error(err)) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) } - fmt.Println(user) - // сохраняем в бд - signature, err := r.service.StoreRecoveryRecord("user") + + 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.SendRecoveryEmail(email, signature) + + err = r.service.RecoveryEmailTask(c.Context(), user.ID.Hex(), email, key) 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"}) @@ -57,9 +56,9 @@ func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { // HandleRecoveryLink обрабатывает ссылку восстановления и обменивает ее на токены func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error { - signature := c.Params("sign") + key := c.Params("sign") // тут получается - record, err := r.service.GetRecoveryRecord(signature) + 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"}) @@ -67,7 +66,7 @@ func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error { // проверка на более чем 15 минут if time.Since(record.CreatedAt) > 15*time.Minute { - r.logger.Error("Recovery link expired", zap.String("signature", signature)) + 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/config.go b/internal/initialize/config.go index 540a454..d78b9e1 100644 --- a/internal/initialize/config.go +++ b/internal/initialize/config.go @@ -8,15 +8,18 @@ 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:"localhost"` - MongoPort string `env:"MONGO_PORT" envDefault:"27017"` - MongoUser string `env:"MONGO_USER" envDefault:"admin"` - MongoPassword string `env:"MONGO_PASSWORD" envDefault:"admin"` - MongoDatabase string `env:"MONGO_DB" envDefault:"codeword_db"` + 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" envDefault:"test"` - PrivateCurveKey string `env:"PRIVATE_CURVE_KEY" envDefault:"test"` - SignSecret string `env:"SIGN_SECRET" envDefault:"test"` + PublicCurveKey string `env:"PUBLIC_CURVE_KEY" envDefault:"localhost:6379"` + PrivateCurveKey string `env:"PRIVATE_CURVE_KEY" envDefault:"localhost:6379"` + SignSecret string `env:"SIGN_SECRET" envDefault:"localhost:6379"` + RedisAddr string `env:"REDIS_ADDR" envDefault:"localhost:6379"` + RedisPassword string `env:"REDIS_PASS" envDefault:"admin"` + RedisDB int `env:"REDIS_DB" envDefault:"2"` } func LoadConfig() (*Config, error) { diff --git a/internal/initialize/redis.go b/internal/initialize/redis.go new file mode 100644 index 0000000..9b1365e --- /dev/null +++ b/internal/initialize/redis.go @@ -0,0 +1,21 @@ +package initialize + +import ( + "context" + "github.com/go-redis/redis/v8" +) + +func InitializeRedis(ctx context.Context, cfg Config) (*redis.Client, error) { + rdb := redis.NewClient(&redis.Options{ + Addr: cfg.RedisAddr, + Password: cfg.RedisPassword, + DB: cfg.RedisDB, + }) + + err := rdb.Ping(ctx) + if err != nil { + return nil, err.Err() + } + + return rdb, nil +} diff --git a/internal/models/user.go b/internal/models/user.go index 2efc1fa..1650e1e 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -1,9 +1,20 @@ package models -import "time" +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) type User struct { - // получаем данные из другого сервиса + ID primitive.ObjectID `bson:"_id,omitempty"` + Login string `bson:"login,omitempty"` + Email string `bson:"email,omitempty"` + Password string `bson:"password,omitempty"` + PhoneNumber string `bson:"phoneNumber,omitempty"` + IsDeleted bool `bson:"isDeleted,omitempty"` + CreatedAt time.Time `bson:"createdAt,omitempty"` + UpdatedAt time.Time `bson:"updatedAt,omitempty"` + DeletedAt *time.Time `bson:"deletedAt,omitempty"` } type RestoreRequest struct { @@ -15,3 +26,10 @@ type RestoreRequest struct { Sent bool SentAt time.Time } + +type RecoveryRecord struct { + UserID string `bson:"user_id"` + Email string `bson:"email"` + Key string `bson:"key"` + CreatedAt time.Time `bson:"created_at"` +} diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index ab9ca1b..e67d452 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -3,36 +3,94 @@ package repository 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/mongo" "go.mongodb.org/mongo-driver/mongo/readpref" "time" ) -type UserRepository struct { - db *mongo.Database +type Deps struct { + Mdb *mongo.Collection + Rdb *redis.Client } -//todo реализовать - -func NewUserRepository(db *mongo.Database) *UserRepository { - return &UserRepository{db} +type codewordRepository struct { + mdb *mongo.Collection + rdb *redis.Client } -func (r *UserRepository) FindByEmail(email string) (*models.User, error) { - //todo - return &models.User{}, nil +type userRepository struct { + mdb *mongo.Collection } -func (r *UserRepository) StoreRecoveryRecord(userID, signature string, createdAt time.Time) error { - //todo +func NewUserRepository(deps Deps) *userRepository { + + return &userRepository{mdb: deps.Mdb} +} + +func NewCodewordRepository(deps Deps) *codewordRepository { + + return &codewordRepository{mdb: deps.Mdb, rdb: deps.Rdb} +} + +func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) { + var user models.User + + err := r.mdb.FindOne(ctx, bson.M{"email": email}).Decode(&user) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + return &user, nil +} + +func (r *codewordRepository) StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) error { + record := models.RecoveryRecord{ + UserID: userID, + Email: email, + Key: string(key), + CreatedAt: time.Now(), + } + + _, err := r.mdb.InsertOne(ctx, record) + if err != nil { + return err + } return nil } -func (r *UserRepository) GetRecoveryRecord(signature string) (*models.RestoreRequest, error) { - return &models.RestoreRequest{UserID: "123", Sign: signature, CreatedAt: time.Now()}, nil +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(), + } + + taskBytes, err := json.Marshal(task) + if err != nil { + return err + } + + uniqKey := fmt.Sprintf("needRecovery:%d", time.Now().UnixNano()) + + if err := r.rdb.Set(ctx, uniqKey, taskBytes, 0).Err(); err != nil { + return err + } + + return nil } -func (r *UserRepository) Ping(ctx context.Context) error { - return r.db.Client().Ping(ctx, readpref.Primary()) +func (r *codewordRepository) GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) { + return &models.RestoreRequest{UserID: "123", Sign: key, CreatedAt: time.Now()}, nil +} + +func (r *codewordRepository) Ping(ctx context.Context) error { + return r.mdb.Database().Client().Ping(ctx, readpref.Primary()) } diff --git a/internal/server/http/http_server.go b/internal/server/http/http_server.go index 1e9eb5a..4b0808a 100644 --- a/internal/server/http/http_server.go +++ b/internal/server/http/http_server.go @@ -2,7 +2,6 @@ package http import ( controller "codeword/internal/controller/recovery" - "codeword/internal/repository" "context" "fmt" "github.com/gofiber/fiber/v2" @@ -12,13 +11,11 @@ import ( type ServerConfig struct { Logger *zap.Logger - Repo *repository.UserRepository RecoveryController *controller.RecoveryController } type Server struct { Logger *zap.Logger - Repo *repository.UserRepository RecoveryController *controller.RecoveryController app *fiber.App } @@ -28,7 +25,6 @@ func NewServer(config ServerConfig) *Server { s := &Server{ Logger: config.Logger, - Repo: config.Repo, RecoveryController: config.RecoveryController, app: app, } @@ -61,7 +57,7 @@ func (s *Server) handleLiveness(c *fiber.Ctx) error { func (s *Server) handleReadiness(c *fiber.Ctx) error { startTime := time.Now() - if err := s.Repo.Ping(c.Context()); err != nil { + if err := s.RecoveryController.HandlePingDB(c); err != nil { s.Logger.Error("Failed to ping the database", zap.Error(err)) return c.Status(fiber.StatusServiceUnavailable).SendString("DB ping failed") } diff --git a/internal/services/recovery_service.go b/internal/services/recovery_service.go index 59ab60f..0d5ea87 100644 --- a/internal/services/recovery_service.go +++ b/internal/services/recovery_service.go @@ -3,70 +3,75 @@ package services import ( "codeword/internal/models" "codeword/internal/utils/encrypt" + "context" "go.uber.org/zap" - "time" ) -type UserRepository interface { - FindByEmail(email string) (*models.User, error) - StoreRecoveryRecord(userID string, signature string, createdAt time.Time) error - GetRecoveryRecord(signature string) (*models.RestoreRequest, error) +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 + Ping(ctx context.Context) error + GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) } -type EmailSender interface { - SendRecoveryEmail(email, signature string) error +type UserRepository interface { + FindByEmail(ctx context.Context, email string) (*models.User, error) } type Deps struct { - Logger *zap.Logger - Repository UserRepository - Email EmailSender - EncryptService *encrypt.Encrypt + Logger *zap.Logger + CodewordRepository CodewordRepository + UserRepository UserRepository + EncryptService *encrypt.Encrypt } type RecoveryService struct { - logger *zap.Logger - repository UserRepository - email EmailSender - encryptService *encrypt.Encrypt + logger *zap.Logger + repositoryCodeword CodewordRepository + repositoryUser UserRepository + encryptService *encrypt.Encrypt } func NewRecoveryService(deps Deps) *RecoveryService { return &RecoveryService{ - logger: deps.Logger, - repository: deps.Repository, - email: deps.Email, - encryptService: deps.EncryptService, + logger: deps.Logger, + repositoryCodeword: deps.CodewordRepository, + repositoryUser: deps.UserRepository, + encryptService: deps.EncryptService, } } // GenerateKey генерирует ключ, используя шифрование на основе эллиптической кривой -func (s *RecoveryService) GenerateKey() (string, error) { - // TODO - return "", nil +func (s *RecoveryService) GenerateKey() ([]byte, error) { + key, err := s.encryptService.SignCommonSecret() + if err != nil { + return nil, err + } + return key, nil +} + +func (s *RecoveryService) Ping(ctx context.Context) error { + return s.repositoryCodeword.Ping(ctx) } // FindUserByEmail ищет пользователя по электронной почте -func (s *RecoveryService) FindUserByEmail(email string) (*models.User, error) { - return s.repository.FindByEmail(email) +func (s *RecoveryService) FindUserByEmail(ctx context.Context, email string) (*models.User, error) { + return s.repositoryUser.FindByEmail(ctx, email) } // StoreRecoveryRecord сохраняет запись восстановления в базе данных -func (s *RecoveryService) StoreRecoveryRecord(userID string) (string, error) { - signature := "" - createdAt := time.Now() - err := s.repository.StoreRecoveryRecord(userID, signature, createdAt) - return signature, err -} - -// GetRecoveryRecord получает запись восстановления из базы данных -func (s *RecoveryService) GetRecoveryRecord(signature string) (*models.RestoreRequest, error) { - return s.repository.GetRecoveryRecord(signature) +func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) error { + return s.repositoryCodeword.StoreRecoveryRecord(ctx, userID, email, key) } // SendRecoveryEmail посылает письмо для восстановления доступа пользователю -func (s *RecoveryService) SendRecoveryEmail(email string, signature string) error { - return s.email.SendRecoveryEmail(email, signature) +func (s *RecoveryService) RecoveryEmailTask(ctx context.Context, userID string, email string, key []byte) error { + return s.repositoryCodeword.InsertToQueue(ctx, userID, email, key) +} + +// GetRecoveryRecord получает запись восстановления из базы данных +func (s *RecoveryService) GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) { + return s.repositoryCodeword.GetRecoveryRecord(ctx, key) } // ExchangeForTokens обменивает ссылку восстановления на токены используя сервис аутентификации. diff --git a/pkg/mongo/config.go b/pkg/mongo/config.go index 34d845e..dc809b4 100644 --- a/pkg/mongo/config.go +++ b/pkg/mongo/config.go @@ -7,11 +7,11 @@ import ( ) type Configuration struct { - MongoHost string `env:"MONGO_HOST" envDefault:"localhost"` - MongoPort string `env:"MONGO_PORT" envDefault:"27017"` - MongoUser string `env:"MONGO_USER" envDefault:"admin"` - MongoPassword string `env:"MONGO_PASSWORD" envDefault:"admin"` - MongoDatabase string `env:"MONGO_DB" envDefault:"codeword_db"` + 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"` } diff --git a/pkg/mongo/connection.go b/pkg/mongo/connection.go index 80c22b1..726b277 100644 --- a/pkg/mongo/connection.go +++ b/pkg/mongo/connection.go @@ -30,7 +30,7 @@ func Connect(ctx context.Context, deps *ConnectDeps) (*mongo.Database, error) { connectionOptions := options.Client(). ApplyURI(mongoURI.String()). SetAuth(options.Credential{ - AuthMechanism: "SCRAM-SHA-1", + AuthMechanism: "SCRAM-SHA-256", AuthSource: deps.Configuration.MongoAuth, Username: deps.Configuration.MongoUser, Password: deps.Configuration.MongoPassword, diff --git a/tests/repository_test/repository_test.go b/tests/repository_test/repository_test.go new file mode 100644 index 0000000..566dc99 --- /dev/null +++ b/tests/repository_test/repository_test.go @@ -0,0 +1,91 @@ +package repository_test + +import ( + "codeword/internal/models" + "codeword/internal/repository" + "context" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "log" + "testing" + "time" + + "github.com/pioz/faker" + "github.com/stretchr/testify/assert" +) + +const mongoURI = "mongodb://test:test@127.0.0.1:27020/?authMechanism=SCRAM-SHA-256&authSource=admin&directConnection=true" + +func TestFindByEmail(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) + if err != nil { + log.Fatalf("Failed to connect to MongoDB: %v", err) + } + + defer func() { + if err := client.Disconnect(ctx); err != nil { + log.Fatalf("Failed to disconnect MongoDB client: %v", err) + } + }() + + if err := client.Ping(ctx, nil); err != nil { + log.Fatalf("Failed to ping MongoDB: %v", err) + } + + db := client.Database("admin") + + userRepo := repository.NewUserRepository(repository.Deps{Rdb: nil, Mdb: db.Collection("users")}) + + t.Run("FindByEmail - existing user", func(t *testing.T) { + user, err := userRepo.FindByEmail(ctx, "email@mail.ru") + assert.NoError(t, err) + assert.NotNil(t, user) + assert.Equal(t, "email@mail.ru", user.Email) + }) + + t.Run("FindByEmail - non-existing user", func(t *testing.T) { + user, err := userRepo.FindByEmail(ctx, "nonexisting@example.com") + assert.NoError(t, err) + assert.Nil(t, user) + }) + +} + +func TestStoreRecoveryRecord(t *testing.T) { + ctx := context.Background() + + mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) + require.NoError(t, err) + + defer func() { + _ = mongoClient.Disconnect(ctx) + }() + + database := mongoClient.Database("admin") + codeword := database.Collection("codeword") + _ = codeword.Drop(ctx) + + userRepo := repository.NewCodewordRepository(repository.Deps{Rdb: nil, Mdb: codeword}) + + for i := 0; i < 10; i++ { + userID := faker.String() + email := faker.Email() + key := []byte("test_recovery_key") + + err = userRepo.StoreRecoveryRecord(ctx, userID, email, key) + assert.NoError(t, err) + + var storedRecord models.RecoveryRecord + 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) + } + + _ = database.Drop(ctx) +}