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:
@@ -33,6 +33,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -79,7 +80,7 @@ func main() {
|
|||||||
|
|
||||||
// Gin
|
// Gin
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: fmt.Sprintf(":%s", config.GetConfig().Port),
|
Addr: fmt.Sprintf(":%s", strconv.Itoa(int(config.GetConfig().Port))),
|
||||||
Handler: router,
|
Handler: router,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type Config struct {
|
|||||||
SmtpTimeout uint `mapstructure:"SMTP_TIMEOUT"`
|
SmtpTimeout uint `mapstructure:"SMTP_TIMEOUT"`
|
||||||
|
|
||||||
PasswordMinLength int `mapstructure:"PASSWORD_MIN_LENGTH"`
|
PasswordMinLength int `mapstructure:"PASSWORD_MIN_LENGTH"`
|
||||||
PasswordMaxLength int `mapstructure:"PASSWORD_MIN_LENGTH"`
|
PasswordMaxLength int `mapstructure:"PASSWORD_MAX_LENGTH"`
|
||||||
PasswordCheckNumbers bool `mapstructure:"PASSWORD_CHECK_NUMBERS"`
|
PasswordCheckNumbers bool `mapstructure:"PASSWORD_CHECK_NUMBERS"`
|
||||||
PasswordCheckCharacters bool `mapstructure:"PASSWORD_CHECK_CHARACTERS"`
|
PasswordCheckCharacters bool `mapstructure:"PASSWORD_CHECK_CHARACTERS"`
|
||||||
PasswordCheckCases bool `mapstructure:"PASSWORD_CHECK_CASES"`
|
PasswordCheckCases bool `mapstructure:"PASSWORD_CHECK_CASES"`
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrSmtpDisabled = errors.New("Smtp is not enabled in the config")
|
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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type Tokens struct {
|
|||||||
|
|
||||||
type RegistrationBeginRequest struct {
|
type RegistrationBeginRequest struct {
|
||||||
Username string `json:"username" binding:"required" validate:"username"`
|
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"`
|
Password string `json:"password" binding:"required" validate:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,14 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"easywish/config"
|
||||||
"easywish/internal/database"
|
"easywish/internal/database"
|
||||||
errs "easywish/internal/errors"
|
errs "easywish/internal/errors"
|
||||||
"easywish/internal/models"
|
"easywish/internal/models"
|
||||||
"easywish/internal/utils"
|
"easywish/internal/utils"
|
||||||
"easywish/internal/utils/enums"
|
"easywish/internal/utils/enums"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/jackc/pgerrcode"
|
"github.com/jackc/pgerrcode"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
@@ -39,11 +41,12 @@ type AuthService interface {
|
|||||||
|
|
||||||
type authServiceImpl struct {
|
type authServiceImpl struct {
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
|
smtp SmtpService
|
||||||
dbctx database.DbContext
|
dbctx database.DbContext
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext) AuthService {
|
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _smtp SmtpService) AuthService {
|
||||||
return &authServiceImpl{log: _log, dbctx: _dbctx}
|
return &authServiceImpl{log: _log, dbctx: _dbctx, smtp: _smtp}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) {
|
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{
|
if _, err = db.TXQueries.CreateLoginInformation(db.CTX, database.CreateLoginInformationParams{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
Email: request.Email,
|
Email: utils.NewPointer(request.Email),
|
||||||
PasswordHash: passwordHash, // Hashed in database
|
PasswordHash: passwordHash, // Hashed in database
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|
||||||
@@ -117,15 +120,30 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
|
|||||||
zap.String("username", user.Username),
|
zap.String("username", user.Username),
|
||||||
zap.Int64("id", user.ID))
|
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(
|
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("username", user.Username),
|
||||||
zap.String("code", generatedCode))
|
zap.String("code", generatedCode))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Send verification email
|
helper.Commit()
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
var Module = fx.Module("services",
|
var Module = fx.Module("services",
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
|
NewSmtpService,
|
||||||
NewAuthService,
|
NewAuthService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,10 +17,21 @@
|
|||||||
|
|
||||||
package services
|
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 {
|
type SmtpService interface {
|
||||||
SendEmail(to string, content string)
|
SendEmail(to string, subject, body string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type smtpServiceImpl struct {
|
type smtpServiceImpl struct {
|
||||||
@@ -31,7 +42,102 @@ func NewSmtpService(_log *zap.Logger) SmtpService {
|
|||||||
return &smtpServiceImpl{log: _log}
|
return &smtpServiceImpl{log: _log}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpServiceImpl) SendEmail(to string, content string) {
|
func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
|
||||||
panic("unimplemented")
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,10 +16,34 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
environment:
|
environment:
|
||||||
|
ENVIRONMENT: ${ENVIRONMENT}
|
||||||
|
HOSTNAME: ${HOSTNAME}
|
||||||
|
PORT: ${PORT}
|
||||||
POSTGRES_URL: ${POSTGRES_URL}
|
POSTGRES_URL: ${POSTGRES_URL}
|
||||||
REDIS_URL: ${REDIS_URL}
|
REDIS_URL: ${REDIS_URL}
|
||||||
MINIO_URL: ${MINIO_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:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
Reference in New Issue
Block a user