feat: implemented smtp service;

feat: implemented registration emails;
fix: config variables for password length used the same env variable;
refactor: all available config variables added to docker-compose.yml
This commit is contained in:
2025-07-09 23:26:30 +03:00
parent 63b63038d1
commit 15c140db31
8 changed files with 171 additions and 21 deletions

View File

@@ -33,6 +33,7 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -79,7 +80,7 @@ func main() {
// Gin
server := &http.Server{
Addr: fmt.Sprintf(":%s", config.GetConfig().Port),
Addr: fmt.Sprintf(":%s", strconv.Itoa(int(config.GetConfig().Port))),
Handler: router,
}

View File

@@ -50,7 +50,7 @@ type Config struct {
SmtpTimeout uint `mapstructure:"SMTP_TIMEOUT"`
PasswordMinLength int `mapstructure:"PASSWORD_MIN_LENGTH"`
PasswordMaxLength int `mapstructure:"PASSWORD_MIN_LENGTH"`
PasswordMaxLength int `mapstructure:"PASSWORD_MAX_LENGTH"`
PasswordCheckNumbers bool `mapstructure:"PASSWORD_CHECK_NUMBERS"`
PasswordCheckCharacters bool `mapstructure:"PASSWORD_CHECK_CHARACTERS"`
PasswordCheckCases bool `mapstructure:"PASSWORD_CHECK_CASES"`

View File

@@ -6,5 +6,5 @@ import (
var (
ErrSmtpDisabled = errors.New("Smtp is not enabled in the config")
ErrSmtpSendFailure = errors.New("Failed to send email")
ErrSmtpMissingConfiguration = errors.New("Some necessary SMTP configuration is missing")
)

View File

@@ -24,7 +24,7 @@ type Tokens struct {
type RegistrationBeginRequest struct {
Username string `json:"username" binding:"required" validate:"username"`
Email *string `json:"email" binding:"email"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required" validate:"password"`
}

View File

@@ -18,12 +18,14 @@
package services
import (
"easywish/config"
"easywish/internal/database"
errs "easywish/internal/errors"
"easywish/internal/models"
"easywish/internal/utils"
"easywish/internal/utils/enums"
"errors"
"fmt"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5"
@@ -39,11 +41,12 @@ type AuthService interface {
type authServiceImpl struct {
log *zap.Logger
smtp SmtpService
dbctx database.DbContext
}
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext) AuthService {
return &authServiceImpl{log: _log, dbctx: _dbctx}
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _smtp SmtpService) AuthService {
return &authServiceImpl{log: _log, dbctx: _dbctx, smtp: _smtp}
}
func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) {
@@ -81,7 +84,7 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
if _, err = db.TXQueries.CreateLoginInformation(db.CTX, database.CreateLoginInformationParams{
UserID: user.ID,
Email: request.Email,
Email: utils.NewPointer(request.Email),
PasswordHash: passwordHash, // Hashed in database
}); err != nil {
@@ -117,15 +120,30 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
zap.String("username", user.Username),
zap.Int64("id", user.ID))
helper.Commit()
if config.GetConfig().SmtpEnabled {
// TODO: get rid of this when email verification will start working
if err := a.smtp.SendEmail(
request.Email,
"Easywish",
fmt.Sprintf("Your registration code is %s", generatedCode),
); err != nil {
a.log.Error(
"Failed to send registration email",
zap.String("email", request.Email),
zap.String("username", request.Username),
zap.Error(err))
return false, errs.ErrServerError
}
} else {
a.log.Debug(
"Declated registration code for a new user",
"Declated registration code for a new user. Enable SMTP in the config to disable this message",
zap.String("username", user.Username),
zap.String("code", generatedCode))
}
// TODO: Send verification email
helper.Commit()
return true, nil
}

View File

@@ -23,6 +23,7 @@ import (
var Module = fx.Module("services",
fx.Provide(
NewSmtpService,
NewAuthService,
),
)

View File

@@ -17,10 +17,21 @@
package services
import "go.uber.org/zap"
import (
"crypto/tls"
"easywish/config"
"fmt"
"net"
"net/smtp"
"strings"
"time"
"go.uber.org/zap"
errs "easywish/internal/errors"
)
type SmtpService interface {
SendEmail(to string, content string)
SendEmail(to string, subject, body string) error
}
type smtpServiceImpl struct {
@@ -31,7 +42,102 @@ func NewSmtpService(_log *zap.Logger) SmtpService {
return &smtpServiceImpl{log: _log}
}
func (s *smtpServiceImpl) SendEmail(to string, content string) {
panic("unimplemented")
func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
cfg := config.GetConfig()
if !cfg.SmtpEnabled {
s.log.Error("Attempted to send an email with SMTP disabled in the config")
return errs.ErrSmtpDisabled
}
if cfg.SmtpServer == "" || cfg.SmtpPort == 0 || cfg.SmtpFrom == "" {
s.log.Error("SMTP service settings or the SMTP From paramater are not set")
return errs.ErrSmtpMissingConfiguration
}
toSlice := []string{to}
headers := map[string]string{
"From": cfg.SmtpFrom,
"To": strings.Join(toSlice, ", "),
"Subject": subject,
"MIME-Version": "1.0",
"Content-Type": "text/html; charset=UTF-8",
}
var sb strings.Builder
for k, v := range headers {
sb.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
}
sb.WriteString("\r\n" + body)
message := []byte(sb.String())
hostPort := fmt.Sprintf("%s:%d", cfg.SmtpServer, cfg.SmtpPort)
var conn net.Conn
var err error
if cfg.SmtpUseSSL {
tlsConfig := &tls.Config{ServerName: cfg.SmtpServer}
conn, err = tls.Dial("tcp", hostPort, tlsConfig)
} else {
timeout := time.Duration(cfg.SmtpTimeout) * time.Second
conn, err = net.DialTimeout("tcp", hostPort, timeout)
}
if err != nil {
s.log.Error("SMTP connection failure", zap.Error(err))
return err
}
defer conn.Close()
client, err := smtp.NewClient(conn, cfg.SmtpServer)
if err != nil {
s.log.Error("SMTP client creation failed", zap.Error(err))
return err
}
defer client.Close()
if !cfg.SmtpUseSSL && cfg.SmtpUseTLS {
tlsConfig := &tls.Config{ServerName: cfg.SmtpServer}
if err = client.StartTLS(tlsConfig); err != nil {
return err
}
}
// Authenticate if credentials exist
if cfg.SmtpUser != "" && cfg.SmtpPassword != "" {
auth := smtp.PlainAuth("", cfg.SmtpUser, cfg.SmtpPassword, cfg.SmtpServer)
if err = client.Auth(auth); err != nil {
s.log.Error("SMTP authentication failure", zap.Error(err))
return err
}
}
if err = client.Mail(cfg.SmtpFrom); err != nil {
s.log.Error("SMTP sender set failed", zap.Error(err))
return err
}
for _, recipient := range to {
if err = client.Rcpt(string(recipient)); err != nil {
s.log.Error("SMTP recipient set failed", zap.Error(err))
return err
}
}
// Send email body
w, err := client.Data()
if err != nil {
s.log.Error("SMTP data command failed", zap.Error(err))
return err
}
if _, err = w.Write(message); err != nil {
s.log.Error("SMTP message write failed", zap.Error(err))
return err
}
if err = w.Close(); err != nil {
s.log.Error("SMTP message close failed", zap.Error(err))
return err
}
return client.Quit()
}

View File

@@ -16,10 +16,34 @@ services:
timeout: 10s
retries: 3
environment:
ENVIRONMENT: ${ENVIRONMENT}
HOSTNAME: ${HOSTNAME}
PORT: ${PORT}
POSTGRES_URL: ${POSTGRES_URL}
REDIS_URL: ${REDIS_URL}
MINIO_URL: ${MINIO_URL}
ENVIRONMENT: ${ENVIRONMENT}
JWT_ALGORITHM: ${JWT_ALGORITHM}
JWT_SECRET: ${JWT_SECRET}
JWT_ISSUER: ${JWT_ISSUER}
JWT_AUDIENCE: ${JWT_AUDIENCE}
JWT_EXP_ACCESS: ${JWT_EXP_ACCESS}
JWT_EXP_REFRESH: ${JWT_EXP_REFRESH}
SMTP_ENABLED: ${SMTP_ENABLED}
SMTP_SERVER: ${SMTP_SERVER}
SMTP_PORT: ${SMTP_PORT}
SMTP_USER: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_FROM: ${SMTP_FROM}
SMTP_USE_TLS: ${SMTP_USE_TLS}
SMTP_USE_SSL: ${SMTP_USE_SSL}
SMTP_TIMEOUT: ${SMTP_TIMEOUT}
PASSWORD_MIN_LENGTH: ${PASSWORD_MIN_LENGTH}
PASSWORD_MAX_LENGTH: ${PASSWORD_MAX_LENGTH}
PASSWORD_CHECK_NUMBER: ${PASSWORD_CHECK_NUMBER}
PASSWORD_CHECK_CHARAC: ${PASSWORD_CHECK_CHARAC}
PASSWORD_CHECK_CASES: ${PASSWORD_CHECK_CASES}
PASSWORD_CHECK_SYMBOL: ${PASSWORD_CHECK_SYMBOL}
PASSWORD_CHECK_LEAKED: ${PASSWORD_CHECK_LEAKED}
ports:
- "8080:8080"
networks: