From c011491962b6abbf23d9430e5030c2840d60728d Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Sat, 15 Feb 2025 06:50:25 +0300 Subject: [PATCH 1/9] add basic user integration test --- tests/api.py | 88 ++++++++++++++++++++++++++++++++++ tests/integration/test_user.py | 33 +++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 tests/api.py create mode 100644 tests/integration/test_user.py diff --git a/tests/api.py b/tests/api.py new file mode 100644 index 0000000..b0e132f --- /dev/null +++ b/tests/api.py @@ -0,0 +1,88 @@ +import random +import string +import requests + +class Requests(): + def __init__(self, baseUrl): + self.baseUrl = baseUrl + + def post(self, path, json = {}): + return requests.post(self.baseUrl + path, json=json) + +class Auth(): + token: string + + def __init__(self, token): + self.token = token + +class User(): + id: string + email: string + name: string + password: string + + def __init__(self, email, password, name, id="", token = ""): + self.email = email + self.password = password + self.name = name + self.token = token + + +class BackendApi(): + def __init__(self, httpClient): + self.httpClient = httpClient + + def parse_response(self, response): + if response.status != 200: + raise AssertionError('Request error') + + json = response.json() + if json['status'] == 'success': + if 'result' in json: + return json['result'] + return None + + error = json['error'] + raise AssertionError(error['id'], error['message']) + + def user_create(self, user: User | None) -> User: + if user == None: + email = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + '@test.test' + name = ''.join(random.choices(string.ascii_letters, k=10)) + password = 'Abcdef1!!1' + user = User(email, password, name) + + res = self.parse_response( + self.httpClient.post( + "/v1/user/create", json={ + "email": user.email, + "password": user.password, + "name": user.name, + } + ) + ) + + return User(res['email'], res['password'], res['name'], res['id']) + + def user_login(self, user: User) -> Auth: + res = self.parse_response( + self.httpClient.post( + "/v1/user/login", json={ + "email": user.email+"a", + "password": user.password, + }, + ) + ) + + return Auth(res['status']) + + def dummy_get(self, auth: Auth): + headers = {"X-Auth": auth.token} + response = self.httpClient.get("/v1/dummy", headers=headers) + if response.status_code != 200: + raise AssertionError('something wrong') + + def health_get(self): + response = self.httpClient.get("/health") + if response.status_code != 200: + raise AssertionError('something wrong') diff --git a/tests/integration/test_user.py b/tests/integration/test_user.py new file mode 100644 index 0000000..b63c935 --- /dev/null +++ b/tests/integration/test_user.py @@ -0,0 +1,33 @@ +import pytest +from api import BackendApi, Requests, User + +backendUrl = "http://localhost:8080" + +class TestUser: + def test_create_user(self): + api = BackendApi(Requests(backendUrl)) + + user = User("user@example.com", "aaaaaA1!", "SomeName") + userWithBadEmail = User("example.com", "aaaaaA1!", "SomeName") + userWithBadPassword = User("user@example.com", "badPassword", "SomeName") + userWithBadName = User("user@example.com", "aaaaaA1!", "") + + with pytest.raises(Exception) as e: + api.user_create(userWithBadEmail) + raise e + + with pytest.raises(Exception) as e: + api.user_create(userWithBadPassword) + raise e + + with pytest.raises(Exception) as e: + api.user_create(userWithBadName) + raise e + + api.user_create(user) + api.user_login(user) + + def test_login_user(self): + api = BackendApi(Requests(backendUrl)) + user = api.user_create() + api.user_login(user) \ No newline at end of file From f7096afaa575d26642cfa1d8b42172df14c76b45 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Mon, 17 Feb 2025 09:45:46 +0300 Subject: [PATCH 2/9] fix email verifying, add fake smtp server --- cmd/backend/app.go | 1 + cmd/backend/config.yaml | 4 +- .../server/handlers/user_create_handler.go | 5 +- .../server/handlers/user_verify_handler.go | 15 ++++ cmd/backend/server/server.go | 1 + cmd/coworker/config.yaml | 16 +++-- cmd/coworker/main.go | 33 ++++++--- docker-compose.yaml | 29 ++++++-- internal/core/services/user_service.go | 15 +++- internal/http_server/request_log.go | 2 +- tests/api.py | 40 +++++------ tests/integration/test_user.py | 72 ++++++++++++------- tests/performance/dummy.py | 15 ---- tests/performance/health.py | 11 --- 14 files changed, 157 insertions(+), 102 deletions(-) delete mode 100644 tests/performance/dummy.py delete mode 100644 tests/performance/health.py diff --git a/cmd/backend/app.go b/cmd/backend/app.go index 59c1b26..8a9641f 100644 --- a/cmd/backend/app.go +++ b/cmd/backend/app.go @@ -155,6 +155,7 @@ func (a *App) Run(p RunParams) { JwtCache: jwtCache, EventRepo: *eventRepo, ActionTokenRepo: actionTokenRepo, + Logger: logger, }, ) shortlinkService = services.NewShortlinkSevice( diff --git a/cmd/backend/config.yaml b/cmd/backend/config.yaml index d5df5f4..e50060a 100644 --- a/cmd/backend/config.yaml +++ b/cmd/backend/config.yaml @@ -1,5 +1,5 @@ port: 8080 postgres_url: "postgres://postgres:postgres@localhost:5432/postgres" jwt_signing_key: "./jwt_signing_key" -kafka_url: "localhost:9092" -kafka_topic: "backend_events" \ No newline at end of file +kafka_url: "localhost:9091" +kafka_topic: "events" \ No newline at end of file diff --git a/cmd/backend/server/handlers/user_create_handler.go b/cmd/backend/server/handlers/user_create_handler.go index 392bbdb..35eece3 100644 --- a/cmd/backend/server/handlers/user_create_handler.go +++ b/cmd/backend/server/handlers/user_create_handler.go @@ -31,10 +31,9 @@ func NewUserCreateHandler(log logger.Logger, userService services.UserService) g Name: input.Name, }, ) - - out := createUserOutput{} + if err != nil { - return out, err + return createUserOutput{}, err } return createUserOutput{ diff --git a/cmd/backend/server/handlers/user_verify_handler.go b/cmd/backend/server/handlers/user_verify_handler.go index b6a6163..baff1c4 100644 --- a/cmd/backend/server/handlers/user_verify_handler.go +++ b/cmd/backend/server/handlers/user_verify_handler.go @@ -2,7 +2,9 @@ package handlers import ( "backend/internal/core/services" + httpserver "backend/internal/http_server" "backend/pkg/logger" + "context" "html/template" @@ -69,3 +71,16 @@ func NewUserVerifyEmailHandler(log logger.Logger, userService services.UserServi c.Status(200) } } + +type inputSendVerify struct { + Email string `json:"email" validate:"required,email"` +} + +func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input inputSendVerify) (interface{}, error) { + err := userService.SendEmailVerifyEmail(ctx, input.Email) + return nil, err + }, + ) +} diff --git a/cmd/backend/server/server.go b/cmd/backend/server/server.go index 68e0ec0..b4c6ee8 100644 --- a/cmd/backend/server/server.go +++ b/cmd/backend/server/server.go @@ -48,6 +48,7 @@ func NewServer(opts NewServerOpts) *httpserver.Server { { userGroup.POST("/create", handlers.NewUserCreateHandler(opts.Logger, opts.UserService)) userGroup.POST("/login", handlers.NewUserLoginHandler(opts.Logger, opts.UserService)) + userGroup.POST("/send-verify", handlers.NewUserSendVerifyEmailHandler(opts.Logger, opts.UserService)) } diff --git a/cmd/coworker/config.yaml b/cmd/coworker/config.yaml index 4bb1b89..154c9b1 100644 --- a/cmd/coworker/config.yaml +++ b/cmd/coworker/config.yaml @@ -1,11 +1,13 @@ app: - serviceUrl: "https://localhost:8080" + serviceUrl: "http://localhost:8080" kafka: brokers: - - localhost:9092 - topic: backend_events + - localhost:9091 + topic: events + consumerGroupId: AAAA smtp: - server: smtp.yandex.ru - port: 587 - email: "" - password: "" \ No newline at end of file + server: localhost + port: 12333 + login: "maillogin" + password: "mailpass" + email: "coworker@example.com" \ No newline at end of file diff --git a/cmd/coworker/main.go b/cmd/coworker/main.go index a9ca104..bf06f13 100644 --- a/cmd/coworker/main.go +++ b/cmd/coworker/main.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "html/template" "io" "log" "os" @@ -21,19 +22,21 @@ const MSG_TEXT = `

This message was sent because you forgot a password

-

To change a password, use this link

+

To change a password, use thislink

` -func SendEmailForgotPassword(dialer *gomail.Dialer, from, to, link string) error { - msgText := strings.ReplaceAll(MSG_TEXT, "{{Link}}", link) +type HtmlTemplate struct { + Link string +} +func SendEmailForgotPassword(dialer *gomail.Dialer, from, to, body string) error { m := gomail.NewMessage() m.SetHeader("From", m.FormatAddress(from, "Pet Backend")) m.SetHeader("To", to) m.SetHeader("Subject", "Hello!") - m.SetBody("text/html", msgText) + m.SetBody("text/html", body) return dialer.DialAndSend(m) } @@ -53,8 +56,9 @@ type Config struct { SMTP struct { Server string `yaml:"server"` Port int `yaml:"port"` - Email string `yaml:"email"` + Login string `yaml:"login"` Password string `yaml:"password"` + Email string `yaml:"email"` } `yaml:"smtp"` } @@ -71,7 +75,7 @@ func main() { log.Fatal(err.Error()) } - dialer := gomail.NewDialer(config.SMTP.Server, config.SMTP.Port, config.SMTP.Email, config.SMTP.Password) + dialer := gomail.NewDialer(config.SMTP.Server, config.SMTP.Port, config.SMTP.Login, config.SMTP.Password) r := kafka.NewReader(kafka.ReaderConfig{ Brokers: config.Kafka.Brokers, @@ -92,6 +96,11 @@ func main() { logger.Printf("coworker service started\n") + template, err := template.New("verify-email").Parse(MSG_TEXT) + if err != nil { + log.Fatal(err) + } + for { msg, err := r.FetchMessage(ctx) if err == io.EOF { @@ -120,10 +129,16 @@ func main() { continue } - link := fmt.Sprintf("%s/restore-password?token=%s", config.App.ServiceUrl, value.Token) + link := fmt.Sprintf("%s/verify-user?token=%s", config.App.ServiceUrl, value.Token) - if err := SendEmailForgotPassword(dialer, config.SMTP.Email, value.Email, link); err != nil { - log.Fatalf("failed to send email: %s\n", err.Error()) + builder := &strings.Builder{} + if err := template.Execute(builder, HtmlTemplate{link}); err != nil { + log.Printf("failed to execute html template: %s\n", err.Error()) + continue + } + + if err := SendEmailForgotPassword(dialer, config.SMTP.Email, value.Email, builder.String()); err != nil { + log.Printf("failed to send email: %s\n", err.Error()) continue } } diff --git a/docker-compose.yaml b/docker-compose.yaml index 0262ab0..87d56c3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -104,25 +104,26 @@ services: kafka: image: &kafkaImage apache/kafka:3.8.0 healthcheck: - test: ["CMD-SHELL", "/opt/kafka/bin/kafka-cluster.sh cluster-id --bootstrap-server http://127.0.0.1:9092 || exit 1"] + test: ["CMD-SHELL", "/opt/kafka/bin/kafka-cluster.sh cluster-id --bootstrap-server http://kafka:9092 || exit 1"] interval: 1s timeout: 30s retries: 30 environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller - KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: BROKER KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT - KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093 + KAFKA_LISTENERS: BACKEND://0.0.0.0:9091, BROKER://kafka:9092, CONTROLLER://kafka:9093 + KAFKA_ADVERTISED_LISTENERS: BACKEND://localhost:9091, BROKER://kafka:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: BACKEND:PLAINTEXT, BROKER:PLAINTEXT, CONTROLLER:PLAINTEXT + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 KAFKA_NUM_PARTITIONS: 3 ports: - - 9092:9092 + - 9091:9091 kafka-init: image: *kafkaImage @@ -162,6 +163,22 @@ services: exit 0; " + smtp4dev: + image: rnwood/smtp4dev:v3 + restart: always + ports: + - '12332:80' #WebUI + - '12333:25' #SMTP + - '12334:143' #IMAP + # volumes: + # - smtp4dev-data:/smtp4dev + environment: + - ServerOptions__Urls=http://*:80 + - ServerOptions__HostName=localhost + - ServerOptions__TlsMode=None + - RelayOptions__Login=maillogin + - RelayOptions__Password=mailpass + volumes: postgres-volume: grafana-volume: diff --git a/internal/core/services/user_service.go b/internal/core/services/user_service.go index 64cb446..308db2a 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -5,6 +5,7 @@ import ( "backend/internal/core/repos" "backend/internal/core/utils" "backend/pkg/cache" + "backend/pkg/logger" "context" "fmt" "time" @@ -33,7 +34,7 @@ type UserService interface { VerifyEmail(ctx context.Context, actionToken string) error SendEmailForgotPassword(ctx context.Context, userId string) error - SendEmailVerifyEmail(ctx context.Context, userId string) error + SendEmailVerifyEmail(ctx context.Context, email string) error ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error ChangePasswordWithToken(ctx context.Context, userId, actionToken, newPassword string) error @@ -51,6 +52,7 @@ type UserServiceDeps struct { JwtCache cache.Cache[string, string] EventRepo repos.EventRepo ActionTokenRepo repos.ActionTokenRepo + Logger logger.Logger } type userService struct { @@ -91,7 +93,10 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) ( if err != nil { return nil, err } - u.sendEmailVerifyEmail(ctx, result.Id, user.Email) + + if err := u.sendEmailVerifyEmail(ctx, result.Id, user.Email); err != nil { + u.deps.Logger.Error().Err(err).Msg("error occured on sending email") + } u.deps.UserCache.Set(result.Id, *result, cache.Expiration{Ttl: userCacheTtl}) @@ -190,6 +195,12 @@ func (u *userService) SendEmailVerifyEmail(ctx context.Context, email string) er if err != nil { return err } + if user == nil { + return fmt.Errorf("no such user") + } + if user.EmailVerified { + return fmt.Errorf("user already verified") + } return u.sendEmailVerifyEmail(ctx, user.Id, user.Email) } diff --git a/internal/http_server/request_log.go b/internal/http_server/request_log.go index 45123ca..0a32ec3 100644 --- a/internal/http_server/request_log.go +++ b/internal/http_server/request_log.go @@ -44,7 +44,7 @@ func NewRequestLogMiddleware(logger log.Logger, tracer trace.Tracer, prometheus msg := fmt.Sprintf("Request %s %s %d %v", method, path, statusCode, latency) if statusCode >= 200 && statusCode < 400 { - // ctxLogger.Log().Msg(msg) + ctxLogger.Log().Msg(msg) return } diff --git a/tests/api.py b/tests/api.py index b0e132f..58ee186 100644 --- a/tests/api.py +++ b/tests/api.py @@ -11,7 +11,6 @@ class Requests(): class Auth(): token: string - def __init__(self, token): self.token = token @@ -21,21 +20,30 @@ class User(): name: string password: string - def __init__(self, email, password, name, id="", token = ""): + def __init__(self, email, password, name, id=""): + self.id = id self.email = email self.password = password self.name = name - self.token = token + @classmethod + def random(cls): + email = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + '@example.com' + name = ''.join(random.choices(string.ascii_letters, k=10)) + password = 'Abcdef1!!1' + return cls(email, password, name) + +def rand_email(): + return ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + '@example.com' class BackendApi(): def __init__(self, httpClient): self.httpClient = httpClient def parse_response(self, response): - if response.status != 200: + if response.status_code != 200: raise AssertionError('Request error') - + json = response.json() if json['status'] == 'success': if 'result' in json: @@ -45,35 +53,27 @@ class BackendApi(): error = json['error'] raise AssertionError(error['id'], error['message']) - def user_create(self, user: User | None) -> User: - if user == None: - email = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + '@test.test' - name = ''.join(random.choices(string.ascii_letters, k=10)) - password = 'Abcdef1!!1' - user = User(email, password, name) - + def user_create(self, user: User) -> User: res = self.parse_response( self.httpClient.post( - "/v1/user/create", json={ + "/api/v1/user/create", json={ "email": user.email, "password": user.password, "name": user.name, } ) ) - - return User(res['email'], res['password'], res['name'], res['id']) + return User(res['email'], user.password, res['name'], id=res['id']) - def user_login(self, user: User) -> Auth: + def user_login(self, email, password) -> Auth: res = self.parse_response( self.httpClient.post( - "/v1/user/login", json={ - "email": user.email+"a", - "password": user.password, + "/api/v1/user/login", json={ + "email": email, + "password": password, }, ) ) - return Auth(res['status']) def dummy_get(self, auth: Auth): diff --git a/tests/integration/test_user.py b/tests/integration/test_user.py index b63c935..fab6381 100644 --- a/tests/integration/test_user.py +++ b/tests/integration/test_user.py @@ -1,33 +1,53 @@ +import random +import string import pytest -from api import BackendApi, Requests, User +from kafka import KafkaConsumer +from api import BackendApi, Requests, User, rand_email backendUrl = "http://localhost:8080" -class TestUser: - def test_create_user(self): - api = BackendApi(Requests(backendUrl)) +def test_create_user(): + backend = BackendApi(Requests(backendUrl)) - user = User("user@example.com", "aaaaaA1!", "SomeName") - userWithBadEmail = User("example.com", "aaaaaA1!", "SomeName") - userWithBadPassword = User("user@example.com", "badPassword", "SomeName") - userWithBadName = User("user@example.com", "aaaaaA1!", "") + user = User.random() + userWithBadEmail = User("sdfsaadsfgdf", user.password, user.name) + userWithBadPassword = User(user.email, "badPassword", user.name) - with pytest.raises(Exception) as e: - api.user_create(userWithBadEmail) - raise e - - with pytest.raises(Exception) as e: - api.user_create(userWithBadPassword) - raise e - - with pytest.raises(Exception) as e: - api.user_create(userWithBadName) - raise e - - api.user_create(user) - api.user_login(user) + with pytest.raises(Exception): + backend.user_create(userWithBadEmail) + with pytest.raises(Exception): + backend.user_create(userWithBadPassword) + + resultUser = backend.user_create(user) + + #should not create user with same email + with pytest.raises(Exception): + backend.user_create(user) + + assert resultUser.email == user.email + assert resultUser.id != "" + +def test_login_user(): + backend = BackendApi(Requests(backendUrl)) + + # consumer = KafkaConsumer( + # 'backend_events', + # group_id='test-group', + # bootstrap_servers=['localhost:9092'], + # consumer_timeout_ms=1000) + # consumer.seek_to_end() + + user = backend.user_create(User.random()) + + with pytest.raises(Exception): + backend.user_login(user.email, "badpassword") + with pytest.raises(Exception): + backend.user_login(rand_email(), user.password) + + #should not login without verified email + with pytest.raises(Exception): + backend.user_login(user.email, user.password) + + # msgs = consumer.poll(timeout_ms=100) + # print(msgs) - def test_login_user(self): - api = BackendApi(Requests(backendUrl)) - user = api.user_create() - api.user_login(user) \ No newline at end of file diff --git a/tests/performance/dummy.py b/tests/performance/dummy.py deleted file mode 100644 index 8d5f08d..0000000 --- a/tests/performance/dummy.py +++ /dev/null @@ -1,15 +0,0 @@ -from locust import FastHttpUser, task - -from api import BackendApi, Auth - -class DummyGet(FastHttpUser): - def on_start(self): - self.api = BackendApi(self.client) - user = self.api.user_create() - self.auth = self.api.user_login(user) - - @task - def dummy_test(self): - self.api.dummy_get(self.auth) - - \ No newline at end of file diff --git a/tests/performance/health.py b/tests/performance/health.py deleted file mode 100644 index 8c05304..0000000 --- a/tests/performance/health.py +++ /dev/null @@ -1,11 +0,0 @@ -from locust import FastHttpUser, task - -from api import BackendApi - -class HealthGet(FastHttpUser): - def on_start(self): - self.api = BackendApi(self.client) - - @task - def user_create_test(self): - self.api.health_get() \ No newline at end of file From 8f16fe84f2da8e42e31aa32623f551204ee0c1d4 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Mon, 17 Feb 2025 11:12:31 +0300 Subject: [PATCH 3/9] add password restore handler --- .../handlers/{dummy_handler.go => dummy.go} | 0 ...{user_create_handler.go => user_create.go} | 0 .../{user_login_handler.go => user_login.go} | 0 .../server/handlers/user_restore_password.go | 27 +++++++++++++++++++ .../handlers/user_send_restore_password.go | 23 ++++++++++++++++ .../server/handlers/user_send_verify.go | 23 ++++++++++++++++ ...{user_verify_handler.go => user_verify.go} | 15 ----------- internal/core/services/user_service.go | 19 +++++++------ internal/http_server/wrapper.go | 2 +- 9 files changed, 85 insertions(+), 24 deletions(-) rename cmd/backend/server/handlers/{dummy_handler.go => dummy.go} (100%) rename cmd/backend/server/handlers/{user_create_handler.go => user_create.go} (100%) rename cmd/backend/server/handlers/{user_login_handler.go => user_login.go} (100%) create mode 100644 cmd/backend/server/handlers/user_restore_password.go create mode 100644 cmd/backend/server/handlers/user_send_restore_password.go create mode 100644 cmd/backend/server/handlers/user_send_verify.go rename cmd/backend/server/handlers/{user_verify_handler.go => user_verify.go} (74%) diff --git a/cmd/backend/server/handlers/dummy_handler.go b/cmd/backend/server/handlers/dummy.go similarity index 100% rename from cmd/backend/server/handlers/dummy_handler.go rename to cmd/backend/server/handlers/dummy.go diff --git a/cmd/backend/server/handlers/user_create_handler.go b/cmd/backend/server/handlers/user_create.go similarity index 100% rename from cmd/backend/server/handlers/user_create_handler.go rename to cmd/backend/server/handlers/user_create.go diff --git a/cmd/backend/server/handlers/user_login_handler.go b/cmd/backend/server/handlers/user_login.go similarity index 100% rename from cmd/backend/server/handlers/user_login_handler.go rename to cmd/backend/server/handlers/user_login.go diff --git a/cmd/backend/server/handlers/user_restore_password.go b/cmd/backend/server/handlers/user_restore_password.go new file mode 100644 index 0000000..076ef19 --- /dev/null +++ b/cmd/backend/server/handlers/user_restore_password.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "backend/internal/core/services" + httpserver "backend/internal/http_server" + "backend/pkg/logger" + "context" + + "github.com/gin-gonic/gin" +) + +type inputRestorePassword struct { + Token string `json:"token"` + NewPassword string `json:"password"` +} + +func NewUserRestorePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input inputRestorePassword) (interface{}, error) { + err := userService.ChangePasswordWithToken(ctx, input.Token, input.NewPassword) + if err != nil { + return nil, err + } + return nil, nil + }, + ) +} diff --git a/cmd/backend/server/handlers/user_send_restore_password.go b/cmd/backend/server/handlers/user_send_restore_password.go new file mode 100644 index 0000000..00725f8 --- /dev/null +++ b/cmd/backend/server/handlers/user_send_restore_password.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "backend/internal/core/services" + httpserver "backend/internal/http_server" + "backend/pkg/logger" + "context" + + "github.com/gin-gonic/gin" +) + +type inputSendRestorePassword struct { + Email string `json:"email" validate:"required,email"` +} + +func NewUserSendRestorePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input inputSendRestorePassword) (interface{}, error) { + err := userService.SendEmailForgotPassword(ctx, input.Email) + return nil, err + }, + ) +} diff --git a/cmd/backend/server/handlers/user_send_verify.go b/cmd/backend/server/handlers/user_send_verify.go new file mode 100644 index 0000000..cb0bec1 --- /dev/null +++ b/cmd/backend/server/handlers/user_send_verify.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "backend/internal/core/services" + httpserver "backend/internal/http_server" + "backend/pkg/logger" + "context" + + "github.com/gin-gonic/gin" +) + +type inputSendVerify struct { + Email string `json:"email" validate:"required,email"` +} + +func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input inputSendVerify) (interface{}, error) { + err := userService.SendEmailVerifyEmail(ctx, input.Email) + return nil, err + }, + ) +} diff --git a/cmd/backend/server/handlers/user_verify_handler.go b/cmd/backend/server/handlers/user_verify.go similarity index 74% rename from cmd/backend/server/handlers/user_verify_handler.go rename to cmd/backend/server/handlers/user_verify.go index baff1c4..b6a6163 100644 --- a/cmd/backend/server/handlers/user_verify_handler.go +++ b/cmd/backend/server/handlers/user_verify.go @@ -2,9 +2,7 @@ package handlers import ( "backend/internal/core/services" - httpserver "backend/internal/http_server" "backend/pkg/logger" - "context" "html/template" @@ -71,16 +69,3 @@ func NewUserVerifyEmailHandler(log logger.Logger, userService services.UserServi c.Status(200) } } - -type inputSendVerify struct { - Email string `json:"email" validate:"required,email"` -} - -func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { - return httpserver.WrapGin(log, - func(ctx context.Context, input inputSendVerify) (interface{}, error) { - err := userService.SendEmailVerifyEmail(ctx, input.Email) - return nil, err - }, - ) -} diff --git a/internal/core/services/user_service.go b/internal/core/services/user_service.go index 308db2a..2d91add 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -37,7 +37,7 @@ type UserService interface { SendEmailVerifyEmail(ctx context.Context, email string) error ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error - ChangePasswordWithToken(ctx context.Context, userId, actionToken, newPassword string) error + ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error } func NewUserService(deps UserServiceDeps) UserService { @@ -141,7 +141,7 @@ func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error } if err := u.deps.UserRepo.SetUserEmailVerified(ctx, token.UserId); err != nil { - return nil + return err } //TODO: log warnings somehow @@ -205,12 +205,7 @@ func (u *userService) SendEmailVerifyEmail(ctx context.Context, email string) er return u.sendEmailVerifyEmail(ctx, user.Id, user.Email) } -func (u *userService) ChangePasswordWithToken(ctx context.Context, userId, actionToken, newPassword string) error { - user, err := u.getUserById(ctx, userId) - if err != nil { - return err - } - +func (u *userService) ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error { token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetForgotPassword) if err != nil { return err @@ -219,6 +214,14 @@ func (u *userService) ChangePasswordWithToken(ctx context.Context, userId, actio return fmt.Errorf("wrong action token") } + user, err := u.getUserById(ctx, token.UserId) + if err != nil { + return err + } + if user == nil { + return fmt.Errorf("no such user") + } + if err := u.updatePassword(ctx, *user, newPassword); err != nil { return err } diff --git a/internal/http_server/wrapper.go b/internal/http_server/wrapper.go index f96d6a4..efb54c2 100644 --- a/internal/http_server/wrapper.go +++ b/internal/http_server/wrapper.go @@ -12,7 +12,7 @@ type Handler[Input, Output interface{}] func(ctx context.Context, input Input) ( type ResponseOk struct { Status string `json:"status"` - Result interface{} `json:"result"` + Result interface{} `json:"result,omitempty"` } type ResponseError struct { From b4c1004cf2616468b7d45b2b4cb067e95d60d1f4 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Mon, 17 Feb 2025 11:24:37 +0300 Subject: [PATCH 4/9] add restore password endpoints --- .../server/handlers/user_change_password.go | 36 +++++++++++++++++++ cmd/backend/server/middleware/auth.go | 8 +++++ cmd/backend/server/server.go | 15 +++----- cmd/backend/server/utils/user.go | 14 -------- 4 files changed, 48 insertions(+), 25 deletions(-) create mode 100644 cmd/backend/server/handlers/user_change_password.go delete mode 100644 cmd/backend/server/utils/user.go diff --git a/cmd/backend/server/handlers/user_change_password.go b/cmd/backend/server/handlers/user_change_password.go new file mode 100644 index 0000000..87179a9 --- /dev/null +++ b/cmd/backend/server/handlers/user_change_password.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "backend/cmd/backend/server/middleware" + "backend/internal/core/services" + httpserver "backend/internal/http_server" + "backend/pkg/logger" + "context" + "fmt" + + "github.com/gin-gonic/gin" +) + +type inputChangePassword struct { + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword"` +} + +func NewUserChangePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input inputChangePassword) (interface{}, error) { + ginCtx, ok := ctx.(*gin.Context) + if !ok { + return nil, fmt.Errorf("can not cast context") + } + user := middleware.GetUserFromRequest(ginCtx) + + err := userService.ChangePassword(ctx, user.Id, input.OldPassword, input.NewPassword) + if err != nil { + return nil, err + } + + return nil, nil + }, + ) +} diff --git a/cmd/backend/server/middleware/auth.go b/cmd/backend/server/middleware/auth.go index a2f6a6b..b5b94d2 100644 --- a/cmd/backend/server/middleware/auth.go +++ b/cmd/backend/server/middleware/auth.go @@ -1,12 +1,20 @@ package middleware import ( + "backend/internal/core/models" "backend/internal/core/services" "fmt" "github.com/gin-gonic/gin" ) +func GetUserFromRequest(c *gin.Context) *models.UserDTO { + if user, ok := c.Get("user"); ok { + return user.(*models.UserDTO) + } + return nil +} + func NewAuthMiddleware(userService services.UserService) gin.HandlerFunc { return func(ctx *gin.Context) { token := ctx.GetHeader("X-Auth") diff --git a/cmd/backend/server/server.go b/cmd/backend/server/server.go index b4c6ee8..d5071a9 100644 --- a/cmd/backend/server/server.go +++ b/cmd/backend/server/server.go @@ -3,7 +3,6 @@ package server import ( "backend/cmd/backend/server/handlers" "backend/cmd/backend/server/middleware" - "backend/cmd/backend/server/utils" "backend/internal/core/services" httpserver "backend/internal/http_server" "backend/internal/integrations" @@ -49,17 +48,11 @@ func NewServer(opts NewServerOpts) *httpserver.Server { userGroup.POST("/create", handlers.NewUserCreateHandler(opts.Logger, opts.UserService)) userGroup.POST("/login", handlers.NewUserLoginHandler(opts.Logger, opts.UserService)) userGroup.POST("/send-verify", handlers.NewUserSendVerifyEmailHandler(opts.Logger, opts.UserService)) + userGroup.POST("/send-restore-password", handlers.NewUserSendRestorePasswordHandler(opts.Logger, opts.UserService)) + userGroup.POST("/restore-password", handlers.NewUserRestorePasswordHandler(opts.Logger, opts.UserService)) - } - - dummyGroup := v1.Group("/dummy") - dummyGroup.Use(middleware.NewAuthMiddleware(opts.UserService)) - { - dummyGroup.GET("", handlers.NewDummyHandler()) - dummyGroup.POST("/forgot-password", func(c *gin.Context) { - user := utils.GetUserFromRequest(c) - opts.UserService.SendEmailForgotPassword(c, user.Id) - }) + userGroup.Use(middleware.NewAuthMiddleware(opts.UserService)) + userGroup.POST("/change-password", handlers.NewUserChangePasswordHandler(opts.Logger, opts.UserService)) } return httpserver.New( diff --git a/cmd/backend/server/utils/user.go b/cmd/backend/server/utils/user.go deleted file mode 100644 index ebc8db3..0000000 --- a/cmd/backend/server/utils/user.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import ( - "backend/internal/core/models" - - "github.com/gin-gonic/gin" -) - -func GetUserFromRequest(c *gin.Context) *models.UserDTO { - if user, ok := c.Get("user"); ok { - return user.(*models.UserDTO) - } - return nil -} From 0db15640126d20d4b6bca1b8ba4374b22951005c Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Mon, 17 Feb 2025 17:32:44 +0300 Subject: [PATCH 5/9] improve coworker service --- .../server/handlers/user_send_verify.go | 2 +- cmd/coworker/config.go | 30 +++++ cmd/coworker/emailer.go | 87 +++++++++++++ cmd/coworker/main.go | 117 +++++------------- internal/core/repos/event_repo.go | 18 ++- internal/core/services/user_service.go | 26 ++-- internal/http_server/request_log.go | 2 +- 7 files changed, 181 insertions(+), 101 deletions(-) create mode 100644 cmd/coworker/config.go create mode 100644 cmd/coworker/emailer.go diff --git a/cmd/backend/server/handlers/user_send_verify.go b/cmd/backend/server/handlers/user_send_verify.go index cb0bec1..7938d4c 100644 --- a/cmd/backend/server/handlers/user_send_verify.go +++ b/cmd/backend/server/handlers/user_send_verify.go @@ -16,7 +16,7 @@ type inputSendVerify struct { func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { return httpserver.WrapGin(log, func(ctx context.Context, input inputSendVerify) (interface{}, error) { - err := userService.SendEmailVerifyEmail(ctx, input.Email) + err := userService.SendEmailVerifyUser(ctx, input.Email) return nil, err }, ) diff --git a/cmd/coworker/config.go b/cmd/coworker/config.go new file mode 100644 index 0000000..e40f597 --- /dev/null +++ b/cmd/coworker/config.go @@ -0,0 +1,30 @@ +package main + +import "backend/pkg/config" + +func LoadConfig(filePath string) (Config, error) { + return config.NewFromFile[Config](filePath) +} + +type Config struct { + App struct { + LogFile string `yaml:"logFile"` + ServiceUrl string `yaml:"serviceUrl"` + } + + Kafka struct { + Brokers []string `yaml:"brokers"` + Topic string `yaml:"topic"` + ConsumerGroupId string `yaml:"consumerGroupId"` + } `yaml:"kafka"` + + SMTP ConfigSMTP `yaml:"smtp"` +} + +type ConfigSMTP struct { + Server string `yaml:"server"` + Port int `yaml:"port"` + Login string `yaml:"login"` + Password string `yaml:"password"` + Email string `yaml:"email"` +} diff --git a/cmd/coworker/emailer.go b/cmd/coworker/emailer.go new file mode 100644 index 0000000..7c496c6 --- /dev/null +++ b/cmd/coworker/emailer.go @@ -0,0 +1,87 @@ +package main + +import ( + "html/template" + "strings" + + "gopkg.in/gomail.v2" +) + +const MSG_TEXT = ` + + + + +

{{.Text}}

+ {{if .Link}} + Clicklink

+ {{end}} + + +` + +type MailContent struct { + Text string + Link string +} + +func NewEmailer(conf ConfigSMTP) (*Emailer, error) { + dialer := gomail.NewDialer(conf.Server, conf.Port, conf.Login, conf.Password) + + closer, err := dialer.Dial() + if err != nil { + return nil, err + } + defer closer.Close() + + htmlTemplate, err := template.New("verify-email").Parse(MSG_TEXT) + if err != nil { + return nil, err + } + + return &Emailer{ + senderEmail: conf.Email, + htmlTemplate: htmlTemplate, + dialer: dialer, + }, nil +} + +type Emailer struct { + senderEmail string + htmlTemplate *template.Template + dialer *gomail.Dialer +} + +func (e *Emailer) SendRestorePassword(email, token string) error { + return e.sendEmail("Restore your password", email, MailContent{ + Text: "Token: " + token, + }) +} + +func (e *Emailer) SendVerifyUser(email, link string) error { + return e.sendEmail("Verify your email", email, MailContent{ + Text: "You recieved this message due to registration of account. Use this link to verify email:", + Link: link, + }) +} + +func (e *Emailer) SendPasswordChanged(email string) error { + return e.sendEmail("Password changed", email, MailContent{ + Text: "You recieved this message due to password change", + }) +} + +func (e *Emailer) sendEmail(subject, to string, content MailContent) error { + builder := &strings.Builder{} + if err := e.htmlTemplate.Execute(builder, content); err != nil { + return err + } + + m := gomail.NewMessage() + m.SetHeader("From", m.FormatAddress(e.senderEmail, "Pet Backend")) + m.SetHeader("To", to) + m.SetHeader("Subject", subject) + m.SetBody("text/html", builder.String()) + + return e.dialer.DialAndSend(m) +} diff --git a/cmd/coworker/main.go b/cmd/coworker/main.go index bf06f13..147d215 100644 --- a/cmd/coworker/main.go +++ b/cmd/coworker/main.go @@ -5,102 +5,46 @@ import ( "context" "encoding/json" "fmt" - "html/template" "io" "log" - "os" - "strings" "github.com/segmentio/kafka-go" - "gopkg.in/gomail.v2" - "gopkg.in/yaml.v3" ) -const MSG_TEXT = ` - - - - -

This message was sent because you forgot a password

-

To change a password, use thislink

- - -` - -type HtmlTemplate struct { - Link string -} - -func SendEmailForgotPassword(dialer *gomail.Dialer, from, to, body string) error { - m := gomail.NewMessage() - m.SetHeader("From", m.FormatAddress(from, "Pet Backend")) - m.SetHeader("To", to) - m.SetHeader("Subject", "Hello!") - m.SetBody("text/html", body) - - return dialer.DialAndSend(m) -} - -type Config struct { - App struct { - LogFile string `yaml:"logFile"` - ServiceUrl string `yaml:"serviceUrl"` - } - - Kafka struct { - Brokers []string `yaml:"brokers"` - Topic string `yaml:"topic"` - ConsumerGroupId string `yaml:"consumerGroupId"` - } `yaml:"kafka"` - - SMTP struct { - Server string `yaml:"server"` - Port int `yaml:"port"` - Login string `yaml:"login"` - Password string `yaml:"password"` - Email string `yaml:"email"` - } `yaml:"smtp"` +type SendEmailEvent struct { + Email string `json:"email"` + Token string `json:"token"` } func main() { ctx := context.Background() - configFile, err := os.ReadFile("config.yaml") + config, err := LoadConfig("config.yaml") if err != nil { log.Fatal(err.Error()) } - config := &Config{} - if err := yaml.Unmarshal(configFile, config); err != nil { + emailer, err := NewEmailer(config.SMTP) + if err != nil { log.Fatal(err.Error()) } - dialer := gomail.NewDialer(config.SMTP.Server, config.SMTP.Port, config.SMTP.Login, config.SMTP.Password) - r := kafka.NewReader(kafka.ReaderConfig{ Brokers: config.Kafka.Brokers, Topic: config.Kafka.Topic, GroupID: config.Kafka.ConsumerGroupId, }) - logger, err := logger.New( - ctx, - logger.NewLoggerOpts{ - Debug: true, - OutputFile: config.App.LogFile, - }, - ) + logger, err := logger.New(ctx, logger.NewLoggerOpts{ + Debug: true, + OutputFile: config.App.LogFile, + }) if err != nil { log.Fatal(err.Error()) } logger.Printf("coworker service started\n") - template, err := template.New("verify-email").Parse(MSG_TEXT) - if err != nil { - log.Fatal(err) - } - for { msg, err := r.FetchMessage(ctx) if err == io.EOF { @@ -119,27 +63,28 @@ func main() { continue } - value := struct { - Email string `json:"email"` - Token string `json:"token"` - }{} - - if err := json.Unmarshal(msg.Value, &value); err != nil { - log.Fatalf("failed to unmarshal: %s\n", err.Error()) - continue - } - - link := fmt.Sprintf("%s/verify-user?token=%s", config.App.ServiceUrl, value.Token) - - builder := &strings.Builder{} - if err := template.Execute(builder, HtmlTemplate{link}); err != nil { - log.Printf("failed to execute html template: %s\n", err.Error()) - continue - } - - if err := SendEmailForgotPassword(dialer, config.SMTP.Email, value.Email, builder.String()); err != nil { - log.Printf("failed to send email: %s\n", err.Error()) + if err := handleEvent(config, emailer, msg); err != nil { + log.Printf("failed to handle event: %s\n", err.Error()) continue } } } + +func handleEvent(config Config, emailer *Emailer, msg kafka.Message) error { + event := SendEmailEvent{} + if err := json.Unmarshal(msg.Value, &event); err != nil { + return err + } + + switch string(msg.Key) { + case "email_forgot_password": + return emailer.SendRestorePassword(event.Email, event.Token) + case "email_password_changed": + return emailer.SendPasswordChanged(event.Email) + case "email_verify_user": + link := fmt.Sprintf("%s/verify-user?token=%s", config.App.ServiceUrl, event.Token) + return emailer.SendVerifyUser(event.Email, link) + } + + return fmt.Errorf("unknown event type") +} diff --git a/internal/core/repos/event_repo.go b/internal/core/repos/event_repo.go index abba12e..ac3e588 100644 --- a/internal/core/repos/event_repo.go +++ b/internal/core/repos/event_repo.go @@ -6,6 +6,12 @@ import ( "encoding/json" ) +const ( + EventEmailPasswordChanged = "email_password_changed" + EventEmailForgotPassword = "email_forgot_password" + EventEmailVerifyUser = "email_verify_user" +) + func NewEventRepo(kafka *integrations.Kafka) *EventRepo { return &EventRepo{ kafka: kafka, @@ -32,10 +38,14 @@ func (e *EventRepo) sendEmail(ctx context.Context, email, actionToken, eventType return e.kafka.SendMessage(ctx, eventType, valueBytes) } -func (e *EventRepo) SendEmailForgotPassword(ctx context.Context, email, actionToken string) error { - return e.sendEmail(ctx, email, actionToken, "email_forgot_password") +func (e *EventRepo) SendEmailPasswordChanged(ctx context.Context, email string) error { + return e.sendEmail(ctx, email, "", EventEmailPasswordChanged) } -func (e *EventRepo) SendEmailVerifyEmail(ctx context.Context, email, actionToken string) error { - return e.sendEmail(ctx, email, actionToken, "email_verify_email") +func (e *EventRepo) SendEmailForgotPassword(ctx context.Context, email, actionToken string) error { + return e.sendEmail(ctx, email, actionToken, EventEmailForgotPassword) +} + +func (e *EventRepo) SendEmailVerifyUser(ctx context.Context, email, actionToken string) error { + return e.sendEmail(ctx, email, actionToken, EventEmailVerifyUser) } diff --git a/internal/core/services/user_service.go b/internal/core/services/user_service.go index 2d91add..6a47cf3 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -34,7 +34,7 @@ type UserService interface { VerifyEmail(ctx context.Context, actionToken string) error SendEmailForgotPassword(ctx context.Context, userId string) error - SendEmailVerifyEmail(ctx context.Context, email string) error + SendEmailVerifyUser(ctx context.Context, email string) error ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error @@ -94,7 +94,7 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) ( return nil, err } - if err := u.sendEmailVerifyEmail(ctx, result.Id, user.Email); err != nil { + if err := u.sendEmailVerifyUser(ctx, result.Id, user.Email); err != nil { u.deps.Logger.Error().Err(err).Msg("error occured on sending email") } @@ -162,7 +162,7 @@ func (u *userService) SendEmailForgotPassword(ctx context.Context, email string) UserId: user.Id, Value: uuid.New().String(), Target: models.ActionTokenTargetForgotPassword, - Expiration: time.Now().Add(1 * time.Hour), + Expiration: time.Now().Add(15 * time.Minute), }, ) if err != nil { @@ -172,7 +172,7 @@ func (u *userService) SendEmailForgotPassword(ctx context.Context, email string) return u.deps.EventRepo.SendEmailForgotPassword(ctx, user.Email, actionToken.Value) } -func (u *userService) sendEmailVerifyEmail(ctx context.Context, userId, email string) error { +func (u *userService) sendEmailVerifyUser(ctx context.Context, userId, email string) error { actionToken, err := u.deps.ActionTokenRepo.CreateActionToken( ctx, models.ActionTokenDTO{ @@ -186,10 +186,10 @@ func (u *userService) sendEmailVerifyEmail(ctx context.Context, userId, email st return err } - return u.deps.EventRepo.SendEmailVerifyEmail(ctx, email, actionToken.Value) + return u.deps.EventRepo.SendEmailVerifyUser(ctx, email, actionToken.Value) } -func (u *userService) SendEmailVerifyEmail(ctx context.Context, email string) error { +func (u *userService) SendEmailVerifyUser(ctx context.Context, email string) error { //user, err := u.getUserById(ctx, userId) user, err := u.deps.UserRepo.GetUserByEmail(ctx, email) if err != nil { @@ -202,7 +202,7 @@ func (u *userService) SendEmailVerifyEmail(ctx context.Context, email string) er return fmt.Errorf("user already verified") } - return u.sendEmailVerifyEmail(ctx, user.Id, user.Email) + return u.sendEmailVerifyUser(ctx, user.Id, user.Email) } func (u *userService) ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error { @@ -256,10 +256,18 @@ func (u *userService) updatePassword(ctx context.Context, user models.UserDTO, n return err } - return u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{ + if err = u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{ Secret: newSecret, Name: user.Name, - }) + }); err != nil { + return err + } + + if err := u.deps.EventRepo.SendEmailPasswordChanged(ctx, user.Email); err != nil { + u.deps.Logger.Error().Err(err).Msg("error occured on sending email") + } + + return nil } func (u *userService) getUserById(ctx context.Context, userId string) (*models.UserDTO, error) { diff --git a/internal/http_server/request_log.go b/internal/http_server/request_log.go index 0a32ec3..f3c7e5b 100644 --- a/internal/http_server/request_log.go +++ b/internal/http_server/request_log.go @@ -41,7 +41,7 @@ func NewRequestLogMiddleware(logger log.Logger, tracer trace.Tracer, prometheus ctxLogger := logger.WithContext(c) - msg := fmt.Sprintf("Request %s %s %d %v", method, path, statusCode, latency) + msg := fmt.Sprintf("%s %s %d %v", method, path, statusCode, latency) if statusCode >= 200 && statusCode < 400 { ctxLogger.Log().Msg(msg) From b434b8a4551a09b74a1c142b490597cb2b6ba08c Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Thu, 20 Feb 2025 04:13:10 +0300 Subject: [PATCH 6/9] del cache cleanup items limit --- cmd/backend/app.go | 10 ++++------ pkg/cache/cache_inmem.go | 7 +------ pkg/cache/cache_inmem_sharded.go | 5 ++--- pkg/cache/interface.go | 2 +- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/cmd/backend/app.go b/cmd/backend/app.go index 8a9641f..d9cfa49 100644 --- a/cmd/backend/app.go +++ b/cmd/backend/app.go @@ -129,19 +129,17 @@ func (a *App) Run(p RunParams) { // Periodically trigger cache cleanup go func() { - tmr := time.NewTicker(5 * time.Minute) + tmr := time.NewTicker(15 * time.Minute) defer tmr.Stop() - batchSize := 100 - for { select { case <-ctx.Done(): return case <-tmr.C: - userCache.CheckExpired(batchSize) - jwtCache.CheckExpired(batchSize) - linksCache.CheckExpired(batchSize) + userCache.CheckExpired() + jwtCache.CheckExpired() + linksCache.CheckExpired() } } }() diff --git a/pkg/cache/cache_inmem.go b/pkg/cache/cache_inmem.go index ea39127..09e80b2 100644 --- a/pkg/cache/cache_inmem.go +++ b/pkg/cache/cache_inmem.go @@ -78,7 +78,7 @@ func (c *cacheInmem[K, V]) Del(key K) { delete(c.data, key) } -func (c *cacheInmem[K, V]) CheckExpired(batchSize int) { +func (c *cacheInmem[K, V]) CheckExpired() { if len(c.data) == 0 { return } @@ -90,10 +90,5 @@ func (c *cacheInmem[K, V]) CheckExpired(batchSize int) { if time.Now().After(item.Expiration) { delete(c.data, key) } - - batchSize-- - if batchSize <= 0 { - return - } } } diff --git a/pkg/cache/cache_inmem_sharded.go b/pkg/cache/cache_inmem_sharded.go index 72b8f36..b95a39e 100644 --- a/pkg/cache/cache_inmem_sharded.go +++ b/pkg/cache/cache_inmem_sharded.go @@ -45,10 +45,9 @@ func (c *cacheInmemSharded[V]) Del(key string) { c.getShard(key).Del(key) } -func (c *cacheInmemSharded[V]) CheckExpired(batchSize int) { - size := batchSize / c.info.Shards +func (c *cacheInmemSharded[V]) CheckExpired() { for _, shard := range c.shards { - shard.CheckExpired(size) + shard.CheckExpired() } } diff --git a/pkg/cache/interface.go b/pkg/cache/interface.go index 9665b73..3dea428 100644 --- a/pkg/cache/interface.go +++ b/pkg/cache/interface.go @@ -7,5 +7,5 @@ type Cache[K comparable, V any] interface { Set(key K, value V, exp Expiration) Del(key K) - CheckExpired(batchSize int) + CheckExpired() } From 5bcfd95cb3d0b6a6878312021ff0c56cad4e6164 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Thu, 20 Feb 2025 06:28:13 +0300 Subject: [PATCH 7/9] fix validation in request handlers --- cmd/backend/server/handlers/dummy.go | 2 +- cmd/backend/server/handlers/user_change_password.go | 4 ++-- cmd/backend/server/handlers/user_create.go | 8 ++++---- cmd/backend/server/handlers/user_login.go | 4 ++-- cmd/backend/server/handlers/user_restore_password.go | 4 ++-- cmd/backend/server/handlers/user_send_restore_password.go | 2 +- cmd/backend/server/handlers/user_send_verify.go | 2 +- cmd/backend/server/server.go | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/backend/server/handlers/dummy.go b/cmd/backend/server/handlers/dummy.go index 7dc5af3..35ea147 100644 --- a/cmd/backend/server/handlers/dummy.go +++ b/cmd/backend/server/handlers/dummy.go @@ -2,7 +2,7 @@ package handlers import "github.com/gin-gonic/gin" -func NewDummyHandler() gin.HandlerFunc { +func New200OkHandler() gin.HandlerFunc { return func(ctx *gin.Context) { ctx.Status(200) } diff --git a/cmd/backend/server/handlers/user_change_password.go b/cmd/backend/server/handlers/user_change_password.go index 87179a9..80d183d 100644 --- a/cmd/backend/server/handlers/user_change_password.go +++ b/cmd/backend/server/handlers/user_change_password.go @@ -12,8 +12,8 @@ import ( ) type inputChangePassword struct { - OldPassword string `json:"oldPassword"` - NewPassword string `json:"newPassword"` + OldPassword string `json:"oldPassword" binding:"required"` + NewPassword string `json:"newPassword" binding:"required"` } func NewUserChangePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { diff --git a/cmd/backend/server/handlers/user_create.go b/cmd/backend/server/handlers/user_create.go index 35eece3..a65b78f 100644 --- a/cmd/backend/server/handlers/user_create.go +++ b/cmd/backend/server/handlers/user_create.go @@ -10,9 +10,9 @@ import ( ) type createUserInput struct { - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required"` - Name string `json:"name" validate:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` + Name string `json:"name" binding:"required"` } type createUserOutput struct { @@ -31,7 +31,7 @@ func NewUserCreateHandler(log logger.Logger, userService services.UserService) g Name: input.Name, }, ) - + if err != nil { return createUserOutput{}, err } diff --git a/cmd/backend/server/handlers/user_login.go b/cmd/backend/server/handlers/user_login.go index 90193dd..106a1ee 100644 --- a/cmd/backend/server/handlers/user_login.go +++ b/cmd/backend/server/handlers/user_login.go @@ -10,8 +10,8 @@ import ( ) type loginUserInput struct { - Login string `json:"email" validate:"required,email"` - Password string `json:"password"` + Login string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` } type loginUserOutput struct { diff --git a/cmd/backend/server/handlers/user_restore_password.go b/cmd/backend/server/handlers/user_restore_password.go index 076ef19..9d1abb8 100644 --- a/cmd/backend/server/handlers/user_restore_password.go +++ b/cmd/backend/server/handlers/user_restore_password.go @@ -10,8 +10,8 @@ import ( ) type inputRestorePassword struct { - Token string `json:"token"` - NewPassword string `json:"password"` + Token string `json:"token" binding:"required"` + NewPassword string `json:"password" binding:"required"` } func NewUserRestorePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { diff --git a/cmd/backend/server/handlers/user_send_restore_password.go b/cmd/backend/server/handlers/user_send_restore_password.go index 00725f8..a73f849 100644 --- a/cmd/backend/server/handlers/user_send_restore_password.go +++ b/cmd/backend/server/handlers/user_send_restore_password.go @@ -10,7 +10,7 @@ import ( ) type inputSendRestorePassword struct { - Email string `json:"email" validate:"required,email"` + Email string `json:"email" binding:"required,email"` } func NewUserSendRestorePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { diff --git a/cmd/backend/server/handlers/user_send_verify.go b/cmd/backend/server/handlers/user_send_verify.go index 7938d4c..6648b97 100644 --- a/cmd/backend/server/handlers/user_send_verify.go +++ b/cmd/backend/server/handlers/user_send_verify.go @@ -10,7 +10,7 @@ import ( ) type inputSendVerify struct { - Email string `json:"email" validate:"required,email"` + Email string `json:"email" binding:"required,email"` } func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { diff --git a/cmd/backend/server/server.go b/cmd/backend/server/server.go index d5071a9..e76b7d4 100644 --- a/cmd/backend/server/server.go +++ b/cmd/backend/server/server.go @@ -29,7 +29,7 @@ func NewServer(opts NewServerOpts) *httpserver.Server { r.ContextWithFallback = true // Use it to allow getting values from c.Request.Context() // r.Static("/webapp", "./webapp") - r.GET("/health", handlers.NewDummyHandler()) + r.GET("/health", handlers.New200OkHandler()) prometheus := integrations.NewPrometheus() r.Any("/metrics", gin.WrapH(prometheus.GetRequestHandler())) From b816154e3b368ecdb6c972db6c0e93f643a9e67a Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Thu, 20 Feb 2025 06:52:10 +0300 Subject: [PATCH 8/9] increase bcrypt difficulty --- internal/core/utils/password.go | 2 +- tests/performance/shortlink.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/core/utils/password.go b/internal/core/utils/password.go index 304b850..14e19ec 100644 --- a/internal/core/utils/password.go +++ b/internal/core/utils/password.go @@ -26,7 +26,7 @@ type passwordUtil struct { } func (b *passwordUtil) Hash(password string) (string, error) { - bytes, _ := bcrypt.GenerateFromPassword([]byte(password), 8) //bcrypt.DefaultCost) + bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), nil } diff --git a/tests/performance/shortlink.py b/tests/performance/shortlink.py index 8fb0e46..8c30cb1 100644 --- a/tests/performance/shortlink.py +++ b/tests/performance/shortlink.py @@ -7,5 +7,5 @@ class ShortlinkCreate(FastHttpUser): self.api = BackendApi(self.client) @task - def user_create_test(self): + def shortlink_create_test(self): self.api.shortlink_create("https://example.com") \ No newline at end of file From abf0ff77f011ed706ebb4340e0174ccf89b06521 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Thu, 20 Feb 2025 07:03:07 +0300 Subject: [PATCH 9/9] rename notifyer service --- cmd/{coworker => notifyer}/config.go | 0 cmd/{coworker => notifyer}/config.yaml | 4 ++-- cmd/{coworker => notifyer}/emailer.go | 0 cmd/{coworker => notifyer}/main.go | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename cmd/{coworker => notifyer}/config.go (100%) rename cmd/{coworker => notifyer}/config.yaml (73%) rename cmd/{coworker => notifyer}/emailer.go (100%) rename cmd/{coworker => notifyer}/main.go (97%) diff --git a/cmd/coworker/config.go b/cmd/notifyer/config.go similarity index 100% rename from cmd/coworker/config.go rename to cmd/notifyer/config.go diff --git a/cmd/coworker/config.yaml b/cmd/notifyer/config.yaml similarity index 73% rename from cmd/coworker/config.yaml rename to cmd/notifyer/config.yaml index 154c9b1..4de10a5 100644 --- a/cmd/coworker/config.yaml +++ b/cmd/notifyer/config.yaml @@ -4,10 +4,10 @@ kafka: brokers: - localhost:9091 topic: events - consumerGroupId: AAAA + consumerGroupId: notifyer-group smtp: server: localhost port: 12333 login: "maillogin" password: "mailpass" - email: "coworker@example.com" \ No newline at end of file + email: "notifyer@example.com" \ No newline at end of file diff --git a/cmd/coworker/emailer.go b/cmd/notifyer/emailer.go similarity index 100% rename from cmd/coworker/emailer.go rename to cmd/notifyer/emailer.go diff --git a/cmd/coworker/main.go b/cmd/notifyer/main.go similarity index 97% rename from cmd/coworker/main.go rename to cmd/notifyer/main.go index 147d215..c328b89 100644 --- a/cmd/coworker/main.go +++ b/cmd/notifyer/main.go @@ -43,7 +43,7 @@ func main() { log.Fatal(err.Error()) } - logger.Printf("coworker service started\n") + logger.Printf("notifyer service started\n") for { msg, err := r.FetchMessage(ctx)