ref project

This commit is contained in:
Pavel 2023-12-31 15:22:03 +03:00
parent b4ce07515c
commit 34debbdd3e
14 changed files with 359 additions and 107 deletions

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

33
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=

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

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

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

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

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

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

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

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

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

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

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

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