Merge branch '123' into 'main'
123 See merge request pena-services/codeword!5
This commit is contained in:
commit
37888d528a
12
.env
12
.env
@ -1,7 +1,7 @@
|
||||
# General application settings
|
||||
APP_NAME=codeword
|
||||
HTTP_HOST="localhost"
|
||||
HTTP_PORT="8000"
|
||||
HTTP_PORT="8080"
|
||||
|
||||
# MongoDB settings
|
||||
MONGO_HOST="127.0.0.1"
|
||||
@ -17,11 +17,13 @@ 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-----"
|
||||
|
||||
SIGN_SECRET=group
|
||||
# SIGN_SECRET="group"
|
||||
|
||||
SIGN_SECRET="secret"
|
||||
|
||||
# SMTP settings
|
||||
SMTP_API_URL="https://api.smtp.bz/v1/smtp/send"
|
||||
@ -31,3 +33,7 @@ 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"
|
||||
AUTH_EXCHANGE_URL = "http://localhost:8000/auth/exchange"
|
@ -0,0 +1,87 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Codeword Recovery Service API
|
||||
version: 1.0.0
|
||||
description: API for handling password recovery for the Codeword service.
|
||||
|
||||
|
||||
paths:
|
||||
/liveness:
|
||||
get:
|
||||
summary: Роут проверки активности
|
||||
responses:
|
||||
'200':
|
||||
description: Успех – сервис запущен
|
||||
|
||||
/readiness:
|
||||
get:
|
||||
summary: Роут проверки базы данных
|
||||
responses:
|
||||
'200':
|
||||
description: Успех — сервис готов и соединение с БД живо
|
||||
'503':
|
||||
description: Служба недоступна — не удалось выполнить проверку связи с БД
|
||||
|
||||
/recover:
|
||||
post:
|
||||
summary: Запустите процесс восстановления пароля
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: Электронная почта, на которую нужно отправить инструкции по восстановлению
|
||||
Referrer:
|
||||
type: string
|
||||
description: URL-адрес referral, если он доступен
|
||||
RedirectionURL:
|
||||
type: string
|
||||
description: URL-адрес, на который перенаправляется пользователь после отправки электронного письма
|
||||
|
||||
responses:
|
||||
'200':
|
||||
description: Запрос на восстановление принят, и возвращен идентификатор записи восстановления
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Идентификатор запроса на восстановление
|
||||
'404':
|
||||
description: Пользователь не найден по электронной почте
|
||||
'500':
|
||||
description: Внутренняя ошибка сервера – разные причины
|
||||
|
||||
/recover/{sign}:
|
||||
get:
|
||||
summary: Обработать ссылку восстановления, в которой содержится подпись и обменять ее на токены
|
||||
parameters:
|
||||
- in: path
|
||||
name: sign
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Подпись восстановления как часть URL-адреса восстановления
|
||||
responses:
|
||||
'200':
|
||||
description: Восстановление успешно, информация для обмена токенов возвращена
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
accessToken:
|
||||
type: string
|
||||
refreshToken:
|
||||
type: string
|
||||
'406':
|
||||
description: NotAcceptable - срок действия ссылки для восстановления истек или она недействительна
|
||||
'500':
|
||||
description: Внутренняя ошибка сервера – разные причины
|
66
internal/adapters/client/auth.go
Normal file
66
internal/adapters/client/auth.go
Normal file
@ -0,0 +1,66 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"codeword/internal/models"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AuthClientDeps struct {
|
||||
AuthUrl string
|
||||
FiberClient *fiber.Client
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type AuthClient struct {
|
||||
deps AuthClientDeps
|
||||
}
|
||||
|
||||
func NewAuthClient(deps AuthClientDeps) *AuthClient {
|
||||
if deps.FiberClient == nil {
|
||||
deps.FiberClient = fiber.AcquireClient()
|
||||
}
|
||||
return &AuthClient{
|
||||
deps: deps,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthClient) RefreshAuthToken(userID, signature string) (*models.RefreshResponse, error) {
|
||||
body := models.AuthRequestBody{
|
||||
UserID: userID,
|
||||
Signature: signature,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
a.deps.Logger.Error("Failed to encode request body", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agent := a.deps.FiberClient.Post(a.deps.AuthUrl)
|
||||
agent.Set("Content-Type", "application/json").Body(bodyBytes)
|
||||
|
||||
statusCode, resBody, errs := agent.Bytes()
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
a.deps.Logger.Error("Error in exchange auth token request", zap.Error(err))
|
||||
}
|
||||
return nil, fmt.Errorf("request failed: %v", errs)
|
||||
}
|
||||
|
||||
if statusCode != fiber.StatusOK {
|
||||
errorMessage := fmt.Sprintf("received an incorrect response from the authentication service: %d", statusCode)
|
||||
a.deps.Logger.Error(errorMessage, zap.Int("status", statusCode))
|
||||
return nil, fmt.Errorf(errorMessage)
|
||||
}
|
||||
|
||||
var tokens models.RefreshResponse
|
||||
if err := json.Unmarshal(resBody, &tokens); err != nil {
|
||||
a.deps.Logger.Error("failed to unmarshal auth service response", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tokens, nil
|
||||
}
|
@ -9,15 +9,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 {
|
||||
@ -33,16 +35,12 @@ func NewRecoveryEmailSender(deps RecoveryEmailSenderDeps) *RecoveryEmailSender {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RecoveryEmailSender) SendRecoveryEmail(email string, signature []byte) error {
|
||||
func (r *RecoveryEmailSender) SendRecoveryEmail(email string, signature string) error {
|
||||
url := r.deps.SmtpApiUrl
|
||||
|
||||
fmt.Println(email, signature)
|
||||
|
||||
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)
|
||||
message := fmt.Sprintf("http://"+r.deps.CodewordHost+":"+r.deps.CodewordPort+"/recover/%s", signature)
|
||||
|
||||
form := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(form)
|
||||
@ -72,16 +70,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)
|
||||
r.deps.Logger.Error("Ошибка при отправке электронной почты", zap.Error(err))
|
||||
err := fmt.Errorf("the SMTP service returned an error: %s Response body: %s", statusCode, body)
|
||||
r.deps.Logger.Error("Error sending email", 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,69 +1,58 @@
|
||||
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/purge_worker"
|
||||
"codeword/internal/worker/recovery_worker"
|
||||
"context"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.uber.org/zap"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
|
||||
logger.Info("Запуск приложения", zap.String("AppName", cfg.AppName))
|
||||
|
||||
mdb, err := initialize.InitializeMongoDB(ctx, cfg)
|
||||
mdb, err := initialize.MongoDB(ctx, cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to initialize MongoDB", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
rdb, err := initialize.InitializeRedis(ctx, cfg)
|
||||
|
||||
encryptService := encrypt.New(&encrypt.EncryptDeps{
|
||||
PublicKey: cfg.PublicCurveKey,
|
||||
PrivateKey: cfg.PrivateCurveKey,
|
||||
SignSecret: cfg.SignSecret,
|
||||
})
|
||||
|
||||
rdb, err := initialize.Redis(ctx, cfg)
|
||||
encrypt := initialize.Encrypt(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.RecoveryEmailSender(cfg, logger)
|
||||
authClient := initialize.AuthClient(cfg, logger)
|
||||
|
||||
recoveryService := services.NewRecoveryService(services.Deps{
|
||||
Logger: logger,
|
||||
CodewordRepository: codewordRepo,
|
||||
UserRepository: userRepo,
|
||||
EncryptService: encryptService,
|
||||
Encrypt: encrypt,
|
||||
AuthClient: authClient,
|
||||
})
|
||||
|
||||
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"),
|
||||
})
|
||||
|
||||
purgeWC := purge_worker.NewRecoveryWC(purge_worker.Deps{
|
||||
Logger: logger,
|
||||
Mongo: mdb.Collection("codeword"),
|
||||
})
|
||||
|
||||
go recoveryWC.Start(ctx)
|
||||
go purgeWC.Start(ctx)
|
||||
|
||||
server := httpserver.NewServer(httpserver.ServerConfig{
|
||||
Logger: logger,
|
||||
@ -72,50 +61,44 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
|
||||
|
||||
go func() {
|
||||
if err := server.Start(cfg.HTTPHost + ":" + cfg.HTTPPort); err != nil {
|
||||
logger.Error("Ошибка запуска сервера", zap.Error(err))
|
||||
logger.Error("Server startup error", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
if err := shutdownApp(server, mdb, logger); err != nil {
|
||||
if err := shutdownApp(ctx, server, mdb, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("Приложение остановлено")
|
||||
logger.Info("The application has stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO возможно стоит вынести в отдельные файлы или отказаться от разделения на отдельные методы
|
||||
|
||||
func shutdownApp(server *httpserver.Server, mdb *mongo.Database, logger *zap.Logger) error {
|
||||
if err := shutdownHTTPServer(server, logger); err != nil {
|
||||
func shutdownApp(ctx context.Context, server *httpserver.Server, mdb *mongo.Database, logger *zap.Logger) error {
|
||||
if err := shutdownHTTPServer(ctx, server, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := shutdownMongoDB(mdb, logger); err != nil {
|
||||
if err := shutdownMongoDB(ctx, mdb, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func shutdownHTTPServer(server *httpserver.Server, logger *zap.Logger) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
func shutdownHTTPServer(ctx context.Context, server *httpserver.Server, logger *zap.Logger) error {
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
logger.Error("Ошибка при остановке HTTP-сервера", zap.Error(err))
|
||||
logger.Error("Error stopping HTTP server", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func shutdownMongoDB(mdb *mongo.Database, logger *zap.Logger) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
func shutdownMongoDB(ctx context.Context, mdb *mongo.Database, logger *zap.Logger) error {
|
||||
if err := mdb.Client().Disconnect(ctx); err != nil {
|
||||
logger.Error("Ошибка при закрытии соединения с MongoDB", zap.Error(err))
|
||||
logger.Error("Error when closing MongoDB connection", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -1,21 +1,25 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"codeword/internal/models"
|
||||
"codeword/internal/services"
|
||||
"encoding/base64"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
"time"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,11 +30,13 @@ func (r *RecoveryController) HandlePingDB(c *fiber.Ctx) error {
|
||||
// HandleRecoveryRequest обрабатывает запрос на восстановление пароля
|
||||
func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error {
|
||||
email := c.FormValue("email")
|
||||
referralURL := c.Get("Referrer")
|
||||
redirectionURL := c.FormValue("RedirectionURL")
|
||||
|
||||
key, err := r.service.GenerateKey()
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to generate key", zap.Error(err))
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
|
||||
if redirectionURL == "" && referralURL != "" {
|
||||
redirectionURL = referralURL
|
||||
} else if redirectionURL == "" {
|
||||
redirectionURL = r.defaultURL
|
||||
}
|
||||
|
||||
user, err := r.service.FindUserByEmail(c.Context(), email)
|
||||
@ -39,38 +45,52 @@ 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)
|
||||
key, err := r.service.GenerateKey()
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to generate key", zap.Error(err))
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
|
||||
}
|
||||
|
||||
signUrl := redirectionURL + base64.URLEncoding.EncodeToString(key)
|
||||
sign := base64.URLEncoding.EncodeToString(key)
|
||||
|
||||
id, err := r.service.StoreRecoveryRecord(c.Context(), models.StoreRecDeps{UserID: user.ID.Hex(), Email: user.Email, Key: sign, Url: 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"})
|
||||
}
|
||||
|
||||
err = r.service.RecoveryEmailTask(c.Context(), user.ID.Hex(), email, key)
|
||||
signWithID := sign + id // подпись с id записи
|
||||
|
||||
err = r.service.RecoveryEmailTask(c.Context(), models.RecEmailDeps{UserID: user.ID.Hex(), Email: email, SignWithID: signWithID, ID: 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,
|
||||
})
|
||||
}
|
||||
|
||||
// todo тут скорее всего помимо подписи будет передаваться еще что-то, например email пользователя от фронта для поиска в бд
|
||||
|
||||
// HandleRecoveryLink обрабатывает ссылку восстановления и обменивает ее на токены
|
||||
func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error {
|
||||
key := c.Params("sign")
|
||||
// тут получается
|
||||
|
||||
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"})
|
||||
return c.Status(fiber.StatusNotAcceptable).JSON(fiber.Map{"error": "Recovery link expired"})
|
||||
}
|
||||
|
||||
tokens, err := r.service.ExchangeForTokens(record.UserID)
|
||||
tokens, err := r.service.ExchangeForTokens(record.UserID, record.Sign)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to exchange recovery link for tokens", zap.Error(err))
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
|
||||
|
@ -1 +1,3 @@
|
||||
package errors
|
||||
|
||||
// пока не нужен
|
||||
|
31
internal/initialize/clients.go
Normal file
31
internal/initialize/clients.go
Normal file
@ -0,0 +1,31 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"codeword/internal/adapters/client"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func RecoveryEmailSender(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,
|
||||
})
|
||||
}
|
||||
|
||||
func AuthClient(cfg Config, logger *zap.Logger) *client.AuthClient {
|
||||
return client.NewAuthClient(client.AuthClientDeps{
|
||||
AuthUrl: cfg.AuthURL,
|
||||
Logger: logger,
|
||||
FiberClient: &fiber.Client{},
|
||||
})
|
||||
}
|
@ -7,28 +7,30 @@ 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"`
|
||||
AuthURL string `env:"AUTH_EXCHANGE_URL"`
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
|
13
internal/initialize/encrypt.go
Normal file
13
internal/initialize/encrypt.go
Normal file
@ -0,0 +1,13 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"codeword/internal/utils/encrypt"
|
||||
)
|
||||
|
||||
func Encrypt(cfg Config) *encrypt.Encrypt {
|
||||
return encrypt.New(&encrypt.EncryptDeps{
|
||||
PublicKey: cfg.PublicCurveKey,
|
||||
PrivateKey: cfg.PrivateCurveKey,
|
||||
SignSecret: cfg.SignSecret,
|
||||
})
|
||||
}
|
@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func InitializeMongoDB(ctx context.Context, cfg Config) (*mongo.Database, error) {
|
||||
func MongoDB(ctx context.Context, cfg Config) (*mongo.Database, error) {
|
||||
dbConfig := &mdb.Configuration{
|
||||
MongoHost: cfg.MongoHost,
|
||||
MongoPort: cfg.MongoPort,
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
func InitializeRedis(ctx context.Context, cfg Config) (*redis.Client, error) {
|
||||
func Redis(ctx context.Context, cfg Config) (*redis.Client, error) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.RedisAddr,
|
||||
Password: cfg.RedisPassword,
|
||||
|
11
internal/models/auth.go
Normal file
11
internal/models/auth.go
Normal file
@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
type AuthRequestBody struct {
|
||||
UserID string `json:"userId"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type RefreshResponse struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
15
internal/models/deps.go
Normal file
15
internal/models/deps.go
Normal file
@ -0,0 +1,15 @@
|
||||
package models
|
||||
|
||||
type StoreRecDeps struct {
|
||||
UserID string
|
||||
Email string
|
||||
Key string
|
||||
Url string
|
||||
}
|
||||
|
||||
type RecEmailDeps struct {
|
||||
UserID string
|
||||
Email string
|
||||
SignWithID string
|
||||
ID string
|
||||
}
|
@ -18,18 +18,20 @@ 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 string `bson:"sign,omitempty"`
|
||||
SignUrl string `bson:"sign_url,omitempty"`
|
||||
SignID string `bson:"sign_id"`
|
||||
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 string
|
||||
}
|
||||
|
88
internal/repository/codeword_repository.go
Normal file
88
internal/repository/codeword_repository.go
Normal file
@ -0,0 +1,88 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"codeword/internal/models"
|
||||
"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.mongodb.org/mongo-driver/mongo/readpref"
|
||||
"time"
|
||||
)
|
||||
|
||||
type codewordRepository struct {
|
||||
mdb *mongo.Collection
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func NewCodewordRepository(deps Deps) *codewordRepository {
|
||||
|
||||
return &codewordRepository{mdb: deps.Mdb, rdb: deps.Rdb}
|
||||
}
|
||||
|
||||
// сохраняем полученные данные о пользователе и подписи в бд
|
||||
func (r *codewordRepository) StoreRecoveryRecord(ctx context.Context, deps models.StoreRecDeps) (string, error) {
|
||||
newID := primitive.NewObjectID()
|
||||
signID := deps.Key + newID.Hex()
|
||||
record := models.RestoreRequest{
|
||||
ID: newID,
|
||||
UserID: deps.UserID,
|
||||
Email: deps.Email,
|
||||
Sign: deps.Key,
|
||||
SignUrl: deps.Url,
|
||||
SignID: signID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, err := r.mdb.InsertOne(ctx, record)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return newID.Hex(), nil
|
||||
}
|
||||
|
||||
// добавляем в очередь данные для отправки на почту в редис
|
||||
func (r *codewordRepository) InsertToQueue(ctx context.Context, deps models.RecEmailDeps) error {
|
||||
task := models.RecoveryRecord{
|
||||
ID: deps.ID,
|
||||
UserID: deps.UserID,
|
||||
Email: deps.Email,
|
||||
Key: deps.SignWithID,
|
||||
}
|
||||
|
||||
taskBytes, err := json.Marshal(task)
|
||||
if 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 string) (*models.RestoreRequest, error) {
|
||||
var restoreRequest models.RestoreRequest
|
||||
|
||||
filter := bson.M{"sign_id": 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 {
|
||||
if err := r.mdb.Database().Client().Ping(ctx, readpref.Primary()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -3,13 +3,9 @@ package repository
|
||||
import (
|
||||
"codeword/internal/models"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/pioz/faker"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Deps struct {
|
||||
@ -17,11 +13,6 @@ type Deps struct {
|
||||
Rdb *redis.Client
|
||||
}
|
||||
|
||||
type codewordRepository struct {
|
||||
mdb *mongo.Collection
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
mdb *mongo.Collection
|
||||
}
|
||||
@ -31,11 +22,7 @@ func NewUserRepository(deps Deps) *userRepository {
|
||||
return &userRepository{mdb: deps.Mdb}
|
||||
}
|
||||
|
||||
func NewCodewordRepository(deps Deps) *codewordRepository {
|
||||
|
||||
return &codewordRepository{mdb: deps.Mdb, rdb: deps.Rdb}
|
||||
}
|
||||
|
||||
// ищем пользователя по мейлу в коллекции users
|
||||
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
|
||||
var user models.User
|
||||
|
||||
@ -48,51 +35,3 @@ 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{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Key: key,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, err := r.mdb.InsertOne(ctx, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *codewordRepository) InsertToQueue(ctx context.Context, userID string, email string, key []byte) error {
|
||||
// todo не забыть убрать потом этот цикл
|
||||
for i := 0; i < 10; i++ {
|
||||
|
||||
task := models.RecoveryRecord{
|
||||
UserID: userID + faker.String(),
|
||||
Email: email,
|
||||
Key: key,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
taskBytes, err := json.Marshal(task)
|
||||
if 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 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())
|
||||
}
|
||||
|
@ -35,7 +35,11 @@ func NewServer(config ServerConfig) *Server {
|
||||
}
|
||||
|
||||
func (s *Server) Start(addr string) error {
|
||||
return s.app.Listen(addr)
|
||||
if err := s.app.Listen(addr); err != nil {
|
||||
s.Logger.Error("Failed to start server", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
|
@ -1,15 +1,17 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"codeword/internal/adapters/client"
|
||||
"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) error
|
||||
InsertToQueue(ctx context.Context, userID string, email string, key []byte) error
|
||||
StoreRecoveryRecord(ctx context.Context, deps models.StoreRecDeps) (string, error)
|
||||
InsertToQueue(ctx context.Context, deps models.RecEmailDeps) error
|
||||
Ping(ctx context.Context) error
|
||||
GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error)
|
||||
}
|
||||
@ -22,14 +24,16 @@ type Deps struct {
|
||||
Logger *zap.Logger
|
||||
CodewordRepository CodewordRepository
|
||||
UserRepository UserRepository
|
||||
EncryptService *encrypt.Encrypt
|
||||
Encrypt *encrypt.Encrypt
|
||||
AuthClient *client.AuthClient
|
||||
}
|
||||
|
||||
type RecoveryService struct {
|
||||
logger *zap.Logger
|
||||
repositoryCodeword CodewordRepository
|
||||
repositoryUser UserRepository
|
||||
encryptService *encrypt.Encrypt
|
||||
encrypt *encrypt.Encrypt
|
||||
authClient *client.AuthClient
|
||||
}
|
||||
|
||||
func NewRecoveryService(deps Deps) *RecoveryService {
|
||||
@ -37,45 +41,99 @@ func NewRecoveryService(deps Deps) *RecoveryService {
|
||||
logger: deps.Logger,
|
||||
repositoryCodeword: deps.CodewordRepository,
|
||||
repositoryUser: deps.UserRepository,
|
||||
encryptService: deps.EncryptService,
|
||||
encrypt: deps.Encrypt,
|
||||
authClient: deps.AuthClient,
|
||||
}
|
||||
}
|
||||
|
||||
// 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) error {
|
||||
return s.repositoryCodeword.StoreRecoveryRecord(ctx, userID, email, key)
|
||||
func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, deps models.StoreRecDeps) (string, error) {
|
||||
id, err := s.repositoryCodeword.StoreRecoveryRecord(ctx, models.StoreRecDeps{UserID: deps.UserID, Email: deps.Email, Key: deps.Key, Url: deps.Url})
|
||||
if err != nil {
|
||||
s.logger.Error("Failed save data in mongoDB for email", zap.String("email", deps.Email), zap.Error(err))
|
||||
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)
|
||||
// RecoveryEmailTask посылает письмо для восстановления доступа пользователю
|
||||
func (s *RecoveryService) RecoveryEmailTask(ctx context.Context, deps models.RecEmailDeps) error {
|
||||
err := s.repositoryCodeword.InsertToQueue(ctx, models.RecEmailDeps{UserID: deps.UserID, Email: deps.Email, SignWithID: deps.SignWithID, ID: deps.ID})
|
||||
if err != nil {
|
||||
s.logger.Error("Failed creating a task to send a worker by email", zap.String("email", deps.Email), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRecoveryRecord получает запись восстановления из базы данных
|
||||
func (s *RecoveryService) GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) {
|
||||
return s.repositoryCodeword.GetRecoveryRecord(ctx, key)
|
||||
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
|
||||
}
|
||||
|
||||
byteKey, err := base64.URLEncoding.DecodeString(req.Sign)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to decode string signature to []byte format", zap.String("signature", key), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// сомнительный вариант но как я думаю верный, что false==err
|
||||
result, err := s.encrypt.VerifySignature(byteKey)
|
||||
if err != nil || !result {
|
||||
s.logger.Error("Failed to verify signature", zap.String("signature", key), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// ExchangeForTokens обменивает ссылку восстановления на токены используя сервис аутентификации.
|
||||
func (s *RecoveryService) ExchangeForTokens(userID string) (map[string]string, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
// меняет подпись на токены идя в auth сервис
|
||||
func (s *RecoveryService) ExchangeForTokens(userID string, signature string) (map[string]string, error) {
|
||||
tokens, err := s.authClient.RefreshAuthToken(userID, signature)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to refresh auth token", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"accessToken": tokens.AccessToken,
|
||||
"refreshToken": tokens.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"crypto/ed25519"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@ -46,12 +47,13 @@ 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
|
||||
}
|
||||
|
||||
// TODO подумать над тем чтобы подпись генерилась каждый раз разгая
|
||||
func (receiver *Encrypt) SignCommonSecret() (signature []byte, err error) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
|
56
internal/worker/purge_worker/purge_worker.go
Normal file
56
internal/worker/purge_worker/purge_worker.go
Normal file
@ -0,0 +1,56 @@
|
||||
package purge_worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.uber.org/zap"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Deps struct {
|
||||
Logger *zap.Logger
|
||||
Mongo *mongo.Collection
|
||||
}
|
||||
|
||||
type PurgeWorker struct {
|
||||
logger *zap.Logger
|
||||
mongo *mongo.Collection
|
||||
}
|
||||
|
||||
func NewRecoveryWC(deps Deps) *PurgeWorker {
|
||||
return &PurgeWorker{
|
||||
logger: deps.Logger,
|
||||
mongo: deps.Mongo,
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *PurgeWorker) Start(ctx context.Context) {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
wc.processTasks(ctx)
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *PurgeWorker) processTasks(ctx context.Context) {
|
||||
wc.logger.Info("Checking cleaning records")
|
||||
|
||||
oneHourAgo := time.Now().Add(-1 * time.Hour)
|
||||
|
||||
filter := bson.M{"created_at": bson.M{"$lt": oneHourAgo}}
|
||||
|
||||
result, err := wc.mongo.DeleteMany(ctx, filter)
|
||||
if err != nil {
|
||||
wc.logger.Error("Error when trying to delete old entries", zap.Error(err))
|
||||
} else {
|
||||
wc.logger.Info("Deleted documents", zap.Int64("count", result.DeletedCount))
|
||||
}
|
||||
}
|
@ -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,23 +17,26 @@ type Deps struct {
|
||||
Logger *zap.Logger
|
||||
Redis *redis.Client
|
||||
EmailSender *client.RecoveryEmailSender
|
||||
Mongo *mongo.Collection
|
||||
}
|
||||
|
||||
type recoveryWorker struct {
|
||||
type RecoveryWorker struct {
|
||||
logger *zap.Logger
|
||||
redis *redis.Client
|
||||
emailSender *client.RecoveryEmailSender
|
||||
mongo *mongo.Collection
|
||||
}
|
||||
|
||||
func NewRecoveryWC(deps Deps) *recoveryWorker {
|
||||
return &recoveryWorker{
|
||||
func NewRecoveryWC(deps Deps) *RecoveryWorker {
|
||||
return &RecoveryWorker{
|
||||
logger: deps.Logger,
|
||||
redis: deps.Redis,
|
||||
emailSender: deps.EmailSender,
|
||||
mongo: deps.Mongo,
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *recoveryWorker) Start(ctx context.Context) {
|
||||
func (wc *RecoveryWorker) Start(ctx context.Context) {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
@ -45,7 +51,7 @@ func (wc *recoveryWorker) Start(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *recoveryWorker) processTasks(ctx context.Context) {
|
||||
func (wc *RecoveryWorker) processTasks(ctx context.Context) {
|
||||
result, err := wc.redis.BRPop(ctx, 1*time.Second, "recoveryQueue").Result()
|
||||
if err != nil {
|
||||
if err != redis.Nil {
|
||||
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
@ -16,6 +17,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// todo add another tests
|
||||
|
||||
const mongoURI = "mongodb://test:test@127.0.0.1:27020/?authMechanism=SCRAM-SHA-256&authSource=admin&directConnection=true"
|
||||
|
||||
func TestFindByEmail(t *testing.T) {
|
||||
@ -75,16 +78,17 @@ 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"
|
||||
|
||||
err = userRepo.StoreRecoveryRecord(ctx, userID, email, key)
|
||||
id, err := userRepo.StoreRecoveryRecord(ctx, models.StoreRecDeps{UserID: userID, Email: email, Key: key, Url: "def.url"})
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user