fix email verifying, add fake smtp server
This commit is contained in:
parent
fe538e631f
commit
f7096afaa5
@ -155,6 +155,7 @@ func (a *App) Run(p RunParams) {
|
||||
JwtCache: jwtCache,
|
||||
EventRepo: *eventRepo,
|
||||
ActionTokenRepo: actionTokenRepo,
|
||||
Logger: logger,
|
||||
},
|
||||
)
|
||||
shortlinkService = services.NewShortlinkSevice(
|
||||
|
||||
@ -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"
|
||||
@ -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{
|
||||
|
||||
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
40
tests/api.py
40
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):
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()
|
||||
Loading…
x
Reference in New Issue
Block a user