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
|
# General application settings
|
||||||
APP_NAME=codeword
|
APP_NAME=codeword
|
||||||
HTTP_HOST="localhost"
|
HTTP_HOST="localhost"
|
||||||
HTTP_PORT="8000"
|
HTTP_PORT="8080"
|
||||||
|
|
||||||
# MongoDB settings
|
# MongoDB settings
|
||||||
MONGO_HOST="127.0.0.1"
|
MONGO_HOST="127.0.0.1"
|
||||||
@ -17,11 +17,13 @@ REDIS_PASS="admin"
|
|||||||
REDIS_DB=2
|
REDIS_DB=2
|
||||||
|
|
||||||
# Keys
|
# 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-----"
|
PRIVATE_CURVE_KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIKn0BKwF3vZvODgWAnUIwQhd8de5oZhY48gc23EWfrfs\n-----END PRIVATE KEY-----"
|
||||||
|
|
||||||
SIGN_SECRET=group
|
# SIGN_SECRET="group"
|
||||||
|
|
||||||
|
SIGN_SECRET="secret"
|
||||||
|
|
||||||
# SMTP settings
|
# SMTP settings
|
||||||
SMTP_API_URL="https://api.smtp.bz/v1/smtp/send"
|
SMTP_API_URL="https://api.smtp.bz/v1/smtp/send"
|
||||||
@ -31,3 +33,7 @@ SMTP_UNAME="kotilion.95@gmail.com"
|
|||||||
SMTP_PASS="vWwbCSg4bf0p"
|
SMTP_PASS="vWwbCSg4bf0p"
|
||||||
SMTP_API_KEY="P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev"
|
SMTP_API_KEY="P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev"
|
||||||
SMTP_SENDER="noreply@mailing.pena.digital"
|
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
|
||||||
|
}
|
@ -18,6 +18,8 @@ type RecoveryEmailSenderDeps struct {
|
|||||||
ApiKey string
|
ApiKey string
|
||||||
FiberClient *fiber.Client
|
FiberClient *fiber.Client
|
||||||
Logger *zap.Logger
|
Logger *zap.Logger
|
||||||
|
CodewordHost string
|
||||||
|
CodewordPort string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecoveryEmailSender struct {
|
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
|
url := r.deps.SmtpApiUrl
|
||||||
|
|
||||||
fmt.Println(email, signature)
|
fmt.Println(email, signature)
|
||||||
|
|
||||||
message := fmt.Sprintf("To: %s\r\n"+
|
message := fmt.Sprintf("http://"+r.deps.CodewordHost+":"+r.deps.CodewordPort+"/recover/%s", signature)
|
||||||
"Subject: Восстановление доступа\r\n"+
|
|
||||||
"\r\n"+
|
|
||||||
"Чтобы восстановить доступ, пожалуйста, перейдите по ссылке ниже:\r\n"+
|
|
||||||
" https://hub.pena.digital/codeword/restore/%s\r\n", email, signature)
|
|
||||||
|
|
||||||
form := new(bytes.Buffer)
|
form := new(bytes.Buffer)
|
||||||
writer := multipart.NewWriter(form)
|
writer := multipart.NewWriter(form)
|
||||||
@ -72,16 +70,16 @@ func (r *RecoveryEmailSender) SendRecoveryEmail(email string, signature []byte)
|
|||||||
|
|
||||||
statusCode, body, errs := req.Bytes()
|
statusCode, body, errs := req.Bytes()
|
||||||
if errs != nil {
|
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]
|
return errs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if statusCode != fiber.StatusOK {
|
if statusCode != fiber.StatusOK {
|
||||||
err := fmt.Errorf("SMTP сервис вернул ошибку: %s Тело ответа: %s", statusCode, body)
|
err := fmt.Errorf("the SMTP service returned an error: %s Response body: %s", statusCode, body)
|
||||||
r.deps.Logger.Error("Ошибка при отправке электронной почты", zap.Error(err))
|
r.deps.Logger.Error("Error sending email", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.deps.Logger.Info("Письмо для восстановления отправлено", zap.String("email", email))
|
//r.deps.Logger.Info("Recovery email sent", zap.String("email", email))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,69 +1,58 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"codeword/internal/adapters/client"
|
|
||||||
controller "codeword/internal/controller/recovery"
|
controller "codeword/internal/controller/recovery"
|
||||||
"codeword/internal/initialize"
|
"codeword/internal/initialize"
|
||||||
"codeword/internal/repository"
|
"codeword/internal/repository"
|
||||||
httpserver "codeword/internal/server/http"
|
httpserver "codeword/internal/server/http"
|
||||||
"codeword/internal/services"
|
"codeword/internal/services"
|
||||||
"codeword/internal/utils/encrypt"
|
"codeword/internal/worker/purge_worker"
|
||||||
"codeword/internal/worker/recovery_worker"
|
"codeword/internal/worker/recovery_worker"
|
||||||
"context"
|
"context"
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
|
func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
|
||||||
logger.Info("Запуск приложения", zap.String("AppName", cfg.AppName))
|
logger.Info("Запуск приложения", zap.String("AppName", cfg.AppName))
|
||||||
|
|
||||||
mdb, err := initialize.InitializeMongoDB(ctx, cfg)
|
mdb, err := initialize.MongoDB(ctx, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to initialize MongoDB", zap.Error(err))
|
logger.Error("Failed to initialize MongoDB", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rdb, err := initialize.InitializeRedis(ctx, cfg)
|
rdb, err := initialize.Redis(ctx, cfg)
|
||||||
|
encrypt := initialize.Encrypt(cfg)
|
||||||
encryptService := encrypt.New(&encrypt.EncryptDeps{
|
|
||||||
PublicKey: cfg.PublicCurveKey,
|
|
||||||
PrivateKey: cfg.PrivateCurveKey,
|
|
||||||
SignSecret: cfg.SignSecret,
|
|
||||||
})
|
|
||||||
|
|
||||||
codewordRepo := repository.NewCodewordRepository(repository.Deps{Rdb: rdb, Mdb: mdb.Collection("codeword")})
|
codewordRepo := repository.NewCodewordRepository(repository.Deps{Rdb: rdb, Mdb: mdb.Collection("codeword")})
|
||||||
userRepo := repository.NewUserRepository(repository.Deps{Rdb: nil, Mdb: mdb.Collection("users")})
|
userRepo := repository.NewUserRepository(repository.Deps{Rdb: nil, Mdb: mdb.Collection("users")})
|
||||||
|
recoveryEmailSender := initialize.RecoveryEmailSender(cfg, logger)
|
||||||
recoveryEmailSender := client.NewRecoveryEmailSender(client.RecoveryEmailSenderDeps{
|
authClient := initialize.AuthClient(cfg, logger)
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
recoveryService := services.NewRecoveryService(services.Deps{
|
recoveryService := services.NewRecoveryService(services.Deps{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
CodewordRepository: codewordRepo,
|
CodewordRepository: codewordRepo,
|
||||||
UserRepository: userRepo,
|
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{
|
recoveryWC := recovery_worker.NewRecoveryWC(recovery_worker.Deps{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Redis: rdb,
|
Redis: rdb,
|
||||||
EmailSender: recoveryEmailSender,
|
EmailSender: recoveryEmailSender,
|
||||||
|
Mongo: mdb.Collection("codeword"),
|
||||||
|
})
|
||||||
|
|
||||||
|
purgeWC := purge_worker.NewRecoveryWC(purge_worker.Deps{
|
||||||
|
Logger: logger,
|
||||||
|
Mongo: mdb.Collection("codeword"),
|
||||||
})
|
})
|
||||||
|
|
||||||
go recoveryWC.Start(ctx)
|
go recoveryWC.Start(ctx)
|
||||||
|
go purgeWC.Start(ctx)
|
||||||
|
|
||||||
server := httpserver.NewServer(httpserver.ServerConfig{
|
server := httpserver.NewServer(httpserver.ServerConfig{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
@ -72,50 +61,44 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := server.Start(cfg.HTTPHost + ":" + cfg.HTTPPort); err != nil {
|
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()
|
<-ctx.Done()
|
||||||
|
|
||||||
if err := shutdownApp(server, mdb, logger); err != nil {
|
if err := shutdownApp(ctx, server, mdb, logger); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Info("Приложение остановлено")
|
logger.Info("The application has stopped")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO возможно стоит вынести в отдельные файлы или отказаться от разделения на отдельные методы
|
// TODO возможно стоит вынести в отдельные файлы или отказаться от разделения на отдельные методы
|
||||||
|
|
||||||
func shutdownApp(server *httpserver.Server, mdb *mongo.Database, logger *zap.Logger) error {
|
func shutdownApp(ctx context.Context, server *httpserver.Server, mdb *mongo.Database, logger *zap.Logger) error {
|
||||||
if err := shutdownHTTPServer(server, logger); err != nil {
|
if err := shutdownHTTPServer(ctx, server, logger); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := shutdownMongoDB(mdb, logger); err != nil {
|
if err := shutdownMongoDB(ctx, mdb, logger); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdownHTTPServer(server *httpserver.Server, logger *zap.Logger) error {
|
func shutdownHTTPServer(ctx context.Context, server *httpserver.Server, logger *zap.Logger) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := server.Shutdown(ctx); err != nil {
|
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 err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdownMongoDB(mdb *mongo.Database, logger *zap.Logger) error {
|
func shutdownMongoDB(ctx context.Context, mdb *mongo.Database, logger *zap.Logger) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := mdb.Client().Disconnect(ctx); err != nil {
|
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 err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"codeword/internal/models"
|
||||||
"codeword/internal/services"
|
"codeword/internal/services"
|
||||||
|
"encoding/base64"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"time"
|
"time"
|
||||||
@ -10,12 +12,14 @@ import (
|
|||||||
type RecoveryController struct {
|
type RecoveryController struct {
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
service *services.RecoveryService
|
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{
|
return &RecoveryController{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
service: service,
|
service: service,
|
||||||
|
defaultURL: defaultRedirectionURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,11 +30,13 @@ func (r *RecoveryController) HandlePingDB(c *fiber.Ctx) error {
|
|||||||
// HandleRecoveryRequest обрабатывает запрос на восстановление пароля
|
// HandleRecoveryRequest обрабатывает запрос на восстановление пароля
|
||||||
func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error {
|
func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error {
|
||||||
email := c.FormValue("email")
|
email := c.FormValue("email")
|
||||||
|
referralURL := c.Get("Referrer")
|
||||||
|
redirectionURL := c.FormValue("RedirectionURL")
|
||||||
|
|
||||||
key, err := r.service.GenerateKey()
|
if redirectionURL == "" && referralURL != "" {
|
||||||
if err != nil {
|
redirectionURL = referralURL
|
||||||
r.logger.Error("Failed to generate key", zap.Error(err))
|
} else if redirectionURL == "" {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
|
redirectionURL = r.defaultURL
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := r.service.FindUserByEmail(c.Context(), email)
|
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"})
|
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 {
|
if err != nil {
|
||||||
r.logger.Error("Failed to store recovery record", zap.Error(err))
|
r.logger.Error("Failed to store recovery record", zap.Error(err))
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
|
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 {
|
if err != nil {
|
||||||
r.logger.Error("Failed to send recovery email", zap.Error(err))
|
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.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 обрабатывает ссылку восстановления и обменивает ее на токены
|
// HandleRecoveryLink обрабатывает ссылку восстановления и обменивает ее на токены
|
||||||
func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error {
|
func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error {
|
||||||
key := c.Params("sign")
|
key := c.Params("sign")
|
||||||
// тут получается
|
|
||||||
record, err := r.service.GetRecoveryRecord(c.Context(), key)
|
record, err := r.service.GetRecoveryRecord(c.Context(), key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to get recovery record", zap.Error(err))
|
r.logger.Error("Failed to get recovery record", zap.Error(err))
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// проверка на более чем 15 минут
|
|
||||||
if time.Since(record.CreatedAt) > 15*time.Minute {
|
if time.Since(record.CreatedAt) > 15*time.Minute {
|
||||||
r.logger.Error("Recovery link expired", zap.String("signature", key))
|
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 {
|
if err != nil {
|
||||||
r.logger.Error("Failed to exchange recovery link for tokens", zap.Error(err))
|
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"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
|
||||||
|
@ -1 +1,3 @@
|
|||||||
package errors
|
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{},
|
||||||
|
})
|
||||||
|
}
|
@ -29,6 +29,8 @@ type Config struct {
|
|||||||
SmtpPassword string `env:"SMTP_PASS"`
|
SmtpPassword string `env:"SMTP_PASS"`
|
||||||
SmtpApiKey string `env:"SMTP_API_KEY"`
|
SmtpApiKey string `env:"SMTP_API_KEY"`
|
||||||
SmtpSender string `env:"SMTP_SENDER"`
|
SmtpSender string `env:"SMTP_SENDER"`
|
||||||
|
DefaultRedirectionURL string `env:"DEFAULT_REDIRECTION_URL"`
|
||||||
|
AuthURL string `env:"AUTH_EXCHANGE_URL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() (*Config, error) {
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitializeMongoDB(ctx context.Context, cfg Config) (*mongo.Database, error) {
|
func MongoDB(ctx context.Context, cfg Config) (*mongo.Database, error) {
|
||||||
dbConfig := &mdb.Configuration{
|
dbConfig := &mdb.Configuration{
|
||||||
MongoHost: cfg.MongoHost,
|
MongoHost: cfg.MongoHost,
|
||||||
MongoPort: cfg.MongoPort,
|
MongoPort: cfg.MongoPort,
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/go-redis/redis/v8"
|
"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{
|
rdb := redis.NewClient(&redis.Options{
|
||||||
Addr: cfg.RedisAddr,
|
Addr: cfg.RedisAddr,
|
||||||
Password: cfg.RedisPassword,
|
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 {
|
type RestoreRequest struct {
|
||||||
ID string // xid или ObjectID
|
ID primitive.ObjectID `bson:"_id,omitempty"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time `bson:"created_at,omitempty"`
|
||||||
Sign string // подпись
|
Sign string `bson:"sign,omitempty"`
|
||||||
Email string // email из запроса
|
SignUrl string `bson:"sign_url,omitempty"`
|
||||||
UserID string // айдишник юзера, которого нашли по email
|
SignID string `bson:"sign_id"`
|
||||||
Sent bool
|
Email string `bson:"email,omitempty"`
|
||||||
SentAt time.Time
|
UserID string `bson:"user_id,omitempty"`
|
||||||
|
Sent bool `bson:"sent"`
|
||||||
|
SentAt time.Time `bson:"sent_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecoveryRecord struct {
|
type RecoveryRecord struct {
|
||||||
UserID string `bson:"user_id"`
|
ID string
|
||||||
Email string `bson:"email"`
|
UserID string
|
||||||
Key []byte `bson:"key"`
|
Email string
|
||||||
CreatedAt time.Time `bson:"created_at"`
|
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 (
|
import (
|
||||||
"codeword/internal/models"
|
"codeword/internal/models"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/pioz/faker"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Deps struct {
|
type Deps struct {
|
||||||
@ -17,11 +13,6 @@ type Deps struct {
|
|||||||
Rdb *redis.Client
|
Rdb *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type codewordRepository struct {
|
|
||||||
mdb *mongo.Collection
|
|
||||||
rdb *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type userRepository struct {
|
type userRepository struct {
|
||||||
mdb *mongo.Collection
|
mdb *mongo.Collection
|
||||||
}
|
}
|
||||||
@ -31,11 +22,7 @@ func NewUserRepository(deps Deps) *userRepository {
|
|||||||
return &userRepository{mdb: deps.Mdb}
|
return &userRepository{mdb: deps.Mdb}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCodewordRepository(deps Deps) *codewordRepository {
|
// ищем пользователя по мейлу в коллекции users
|
||||||
|
|
||||||
return &codewordRepository{mdb: deps.Mdb, rdb: deps.Rdb}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
|
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
|
||||||
var user models.User
|
var user models.User
|
||||||
|
|
||||||
@ -48,51 +35,3 @@ func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models
|
|||||||
}
|
}
|
||||||
return &user, nil
|
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 {
|
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 {
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"codeword/internal/adapters/client"
|
||||||
"codeword/internal/models"
|
"codeword/internal/models"
|
||||||
"codeword/internal/utils/encrypt"
|
"codeword/internal/utils/encrypt"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CodewordRepository interface {
|
type CodewordRepository interface {
|
||||||
StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) error
|
StoreRecoveryRecord(ctx context.Context, deps models.StoreRecDeps) (string, error)
|
||||||
InsertToQueue(ctx context.Context, userID string, email string, key []byte) error
|
InsertToQueue(ctx context.Context, deps models.RecEmailDeps) error
|
||||||
Ping(ctx context.Context) error
|
Ping(ctx context.Context) error
|
||||||
GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error)
|
GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error)
|
||||||
}
|
}
|
||||||
@ -22,14 +24,16 @@ type Deps struct {
|
|||||||
Logger *zap.Logger
|
Logger *zap.Logger
|
||||||
CodewordRepository CodewordRepository
|
CodewordRepository CodewordRepository
|
||||||
UserRepository UserRepository
|
UserRepository UserRepository
|
||||||
EncryptService *encrypt.Encrypt
|
Encrypt *encrypt.Encrypt
|
||||||
|
AuthClient *client.AuthClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecoveryService struct {
|
type RecoveryService struct {
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
repositoryCodeword CodewordRepository
|
repositoryCodeword CodewordRepository
|
||||||
repositoryUser UserRepository
|
repositoryUser UserRepository
|
||||||
encryptService *encrypt.Encrypt
|
encrypt *encrypt.Encrypt
|
||||||
|
authClient *client.AuthClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecoveryService(deps Deps) *RecoveryService {
|
func NewRecoveryService(deps Deps) *RecoveryService {
|
||||||
@ -37,45 +41,99 @@ func NewRecoveryService(deps Deps) *RecoveryService {
|
|||||||
logger: deps.Logger,
|
logger: deps.Logger,
|
||||||
repositoryCodeword: deps.CodewordRepository,
|
repositoryCodeword: deps.CodewordRepository,
|
||||||
repositoryUser: deps.UserRepository,
|
repositoryUser: deps.UserRepository,
|
||||||
encryptService: deps.EncryptService,
|
encrypt: deps.Encrypt,
|
||||||
|
authClient: deps.AuthClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateKey генерирует ключ, используя шифрование на основе эллиптической кривой
|
// GenerateKey генерирует ключ, используя шифрование на основе эллиптической кривой
|
||||||
func (s *RecoveryService) GenerateKey() ([]byte, error) {
|
func (s *RecoveryService) GenerateKey() ([]byte, error) {
|
||||||
key, err := s.encryptService.SignCommonSecret()
|
key, err := s.encrypt.SignCommonSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to generate unique key for user", zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// вызывает пингование в бд
|
||||||
func (s *RecoveryService) Ping(ctx context.Context) error {
|
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 ищет пользователя по электронной почте
|
// FindUserByEmail ищет пользователя по электронной почте
|
||||||
func (s *RecoveryService) FindUserByEmail(ctx context.Context, email string) (*models.User, error) {
|
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 сохраняет запись восстановления в базе данных
|
// StoreRecoveryRecord сохраняет запись восстановления в базе данных
|
||||||
func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, userID string, email string, key []byte) error {
|
func (s *RecoveryService) StoreRecoveryRecord(ctx context.Context, deps models.StoreRecDeps) (string, error) {
|
||||||
return s.repositoryCodeword.StoreRecoveryRecord(ctx, userID, email, key)
|
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 посылает письмо для восстановления доступа пользователю
|
// RecoveryEmailTask посылает письмо для восстановления доступа пользователю
|
||||||
func (s *RecoveryService) RecoveryEmailTask(ctx context.Context, userID string, email string, key []byte) error {
|
func (s *RecoveryService) RecoveryEmailTask(ctx context.Context, deps models.RecEmailDeps) error {
|
||||||
return s.repositoryCodeword.InsertToQueue(ctx, userID, email, key)
|
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 получает запись восстановления из базы данных
|
// GetRecoveryRecord получает запись восстановления из базы данных
|
||||||
func (s *RecoveryService) GetRecoveryRecord(ctx context.Context, key string) (*models.RestoreRequest, error) {
|
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 обменивает ссылку восстановления на токены используя сервис аутентификации.
|
// меняет подпись на токены идя в auth сервис
|
||||||
func (s *RecoveryService) ExchangeForTokens(userID string) (map[string]string, error) {
|
func (s *RecoveryService) ExchangeForTokens(userID string, signature string) (map[string]string, error) {
|
||||||
// TODO
|
tokens, err := s.authClient.RefreshAuthToken(userID, signature)
|
||||||
return nil, nil
|
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/ed25519"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,12 +47,13 @@ func (receiver *Encrypt) VerifySignature(signature []byte) (isValid bool, err er
|
|||||||
|
|
||||||
publicKey, ok := rawPublicKey.(ed25519.PublicKey)
|
publicKey, ok := rawPublicKey.(ed25519.PublicKey)
|
||||||
if !ok {
|
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
|
return ed25519.Verify(publicKey, []byte(receiver.signSecret), signature), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO подумать над тем чтобы подпись генерилась каждый раз разгая
|
||||||
func (receiver *Encrypt) SignCommonSecret() (signature []byte, err error) {
|
func (receiver *Encrypt) SignCommonSecret() (signature []byte, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/go-redis/redis/v8"
|
"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"
|
"go.uber.org/zap"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -14,23 +17,26 @@ type Deps struct {
|
|||||||
Logger *zap.Logger
|
Logger *zap.Logger
|
||||||
Redis *redis.Client
|
Redis *redis.Client
|
||||||
EmailSender *client.RecoveryEmailSender
|
EmailSender *client.RecoveryEmailSender
|
||||||
|
Mongo *mongo.Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
type recoveryWorker struct {
|
type RecoveryWorker struct {
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
emailSender *client.RecoveryEmailSender
|
emailSender *client.RecoveryEmailSender
|
||||||
|
mongo *mongo.Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecoveryWC(deps Deps) *recoveryWorker {
|
func NewRecoveryWC(deps Deps) *RecoveryWorker {
|
||||||
return &recoveryWorker{
|
return &RecoveryWorker{
|
||||||
logger: deps.Logger,
|
logger: deps.Logger,
|
||||||
redis: deps.Redis,
|
redis: deps.Redis,
|
||||||
emailSender: deps.EmailSender,
|
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)
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
defer ticker.Stop()
|
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()
|
result, err := wc.redis.BRPop(ctx, 1*time.Second, "recoveryQueue").Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != redis.Nil {
|
if err != redis.Nil {
|
||||||
@ -65,19 +71,46 @@ func (wc *recoveryWorker) processTasks(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = wc.sendRecoveryTask(task)
|
err = wc.sendRecoveryTask(ctx, task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wc.logger.Error("Failed to send recovery task", zap.String("key", result[0]), zap.Error(err))
|
wc.logger.Error("Failed to send recovery task", zap.String("key", result[0]), zap.Error(err))
|
||||||
return
|
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)
|
err := wc.emailSender.SendRecoveryEmail(task.Email, task.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wc.logger.Error("Failed to send recovery email", zap.Error(err))
|
wc.logger.Error("Failed to send recovery email", zap.Error(err))
|
||||||
return 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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"codeword/internal/models"
|
"codeword/internal/models"
|
||||||
"codeword/internal/repository"
|
"codeword/internal/repository"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
@ -16,6 +17,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"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"
|
const mongoURI = "mongodb://test:test@127.0.0.1:27020/?authMechanism=SCRAM-SHA-256&authSource=admin&directConnection=true"
|
||||||
|
|
||||||
func TestFindByEmail(t *testing.T) {
|
func TestFindByEmail(t *testing.T) {
|
||||||
@ -75,16 +78,17 @@ func TestStoreRecoveryRecord(t *testing.T) {
|
|||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
userID := faker.String()
|
userID := faker.String()
|
||||||
email := faker.Email()
|
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)
|
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)
|
err = codeword.FindOne(ctx, bson.M{"user_id": userID}).Decode(&storedRecord)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, email, storedRecord.Email)
|
assert.Equal(t, email, storedRecord.Email)
|
||||||
assert.Equal(t, string(key), storedRecord.Key)
|
assert.Equal(t, key, storedRecord.Sign)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = database.Drop(ctx)
|
_ = database.Drop(ctx)
|
||||||
|
Loading…
Reference in New Issue
Block a user