fix email verifying, add fake smtp server

This commit is contained in:
Sergey Chubaryan 2025-02-17 09:45:46 +03:00
parent fe538e631f
commit f7096afaa5
14 changed files with 157 additions and 102 deletions

View File

@ -155,6 +155,7 @@ func (a *App) Run(p RunParams) {
JwtCache: jwtCache,
EventRepo: *eventRepo,
ActionTokenRepo: actionTokenRepo,
Logger: logger,
},
)
shortlinkService = services.NewShortlinkSevice(

View File

@ -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"
kafka_url: "localhost:9091"
kafka_topic: "events"

View File

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

View File

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

View File

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

View File

@ -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: ""
server: localhost
port: 12333
login: "maillogin"
password: "mailpass"
email: "coworker@example.com"

View File

@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"os"
@ -21,19 +22,21 @@ const MSG_TEXT = `
</head>
<body>
<p>This message was sent because you forgot a password</p>
<p>To change a password, use <a href="{{Link}}"/>this</a> link</p>
<p>To change a password, use <a href="{{.Link}}">this</a>link</p>
</body>
</html>
`
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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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