From 6ca059651c7b0cd180ce7044de44fbaf5d2cdb3f Mon Sep 17 00:00:00 2001 From: Jessica Tarra Date: Tue, 5 Dec 2023 12:22:18 -0300 Subject: [PATCH] Refactor config setup and add background tasks support --- cmd/api/healthcheck.go | 5 ++- cmd/api/helpers.go | 15 ------- cmd/api/main.go | 80 +++++-------------------------------- cmd/api/middleware.go | 8 ++-- cmd/api/routes.go | 2 +- cmd/api/server.go | 4 +- cmd/api/tokens.go | 2 +- cmd/api/users.go | 6 --- internal/config/config.go | 68 +++++++++++++++---------------- internal/errors/errors.go | 68 +++++++++++++++---------------- internal/helpers/helpers.go | 38 ++++++++++++++++++ ms/auth/app/app.go | 33 +++++++++++++-- ms/auth/app/app_test.go | 23 ++++++++++- 13 files changed, 175 insertions(+), 177 deletions(-) create mode 100644 internal/helpers/helpers.go diff --git a/cmd/api/healthcheck.go b/cmd/api/healthcheck.go index 2824dd0..7892a46 100644 --- a/cmd/api/healthcheck.go +++ b/cmd/api/healthcheck.go @@ -1,6 +1,7 @@ package main import ( + "github.com/jessicatarra/greenlight/internal/config" "net/http" ) @@ -9,8 +10,8 @@ func (app *application) healthcheckHandler(writer http.ResponseWriter, request * "status": "available", "system_info": map[string]string{ "status": "available", - "environment": app.config.env, - "version": version, + "environment": app.config.Env, + "version": config.Version, }, } err := app.writeJSON(writer, http.StatusOK, env, nil) diff --git a/cmd/api/helpers.go b/cmd/api/helpers.go index b9a00a4..92e8611 100644 --- a/cmd/api/helpers.go +++ b/cmd/api/helpers.go @@ -123,18 +123,3 @@ func (app *application) readInt(qs url.Values, key string, defaultValue int, v * return i } - -func (app *application) background(fn func()) { - app.wg.Add(1) - - go func() { - defer app.wg.Done() - defer func() { - if err := recover(); err != nil { - app.logger.PrintError(fmt.Errorf("%s", err), nil) - } - }() - - fn() - }() -} diff --git a/cmd/api/main.go b/cmd/api/main.go index dedf5c7..a93b96e 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,51 +2,18 @@ package main import ( "expvar" - "flag" - "fmt" + "github.com/jessicatarra/greenlight/internal/config" "github.com/jessicatarra/greenlight/internal/database" "github.com/jessicatarra/greenlight/internal/jsonlog" "github.com/jessicatarra/greenlight/internal/mailer" "os" "runtime" - "strconv" - "strings" "sync" "time" ) -var ( - version string - port string - env string -) - -type config struct { - port int - env string - db struct { - dsn string - maxOpenConns int - maxIdleConns int - maxIdleTime string - } - smtp struct { - host string - port int - username string - password string - sender string - } - cors struct { - trustedOrigins []string - } - jwt struct { - secret string - } -} - type application struct { - config config + config config.Config logger *jsonlog.Logger models database.Models mailer mailer.Mailer @@ -63,50 +30,21 @@ type application struct { // @in header // @name Authorization func main() { - var cfg config - - intPort, _ := strconv.Atoi(port) - flag.IntVar(&cfg.port, "port", intPort, "API server port") - flag.StringVar(&cfg.env, "env", env, "Environment (development|staging|production)") - flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv( - "DATABASE_URL"), "PostgreSQL DSN") - flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") - flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") - flag.StringVar(&cfg.db.maxIdleTime, "db-max-idle-time", "15m", "PostgreSQL max connection idle time") - - smtpPort, _ := strconv.Atoi(os.Getenv("SMTP_PORT")) - - flag.StringVar(&cfg.smtp.host, "smtp-host", os.Getenv("SMTP_HOST"), "SMTP host") - flag.IntVar(&cfg.smtp.port, "smtp-port", smtpPort, "SMTP port") - flag.StringVar(&cfg.smtp.username, "smtp-username", os.Getenv("SMTP_USERNAME"), "SMTP username") - flag.StringVar(&cfg.smtp.password, "smtp-password", os.Getenv("SMTP_PASSWORD"), "SMTP password") - flag.StringVar(&cfg.smtp.sender, "smtp-sender", os.Getenv("SMTP_SENDER"), "SMTP sender") - - flag.Func("cors-trusted-origins", "Trusted CORS origins (space separated)", func(val string) error { - cfg.cors.trustedOrigins = strings.Fields(val) - return nil - }) - - flag.StringVar(&cfg.jwt.secret, "jwt-secret", os.Getenv("JWT_SECRET"), "JWT secret") - - displayVersion := flag.Bool("version", false, "Display version and exit") - - flag.Parse() + logger := jsonlog.New(os.Stdout, jsonlog.LevelInfo) - if *displayVersion { - fmt.Printf("Version:\t%s\n", version) + cfg, err := config.Init() + if err != nil { + logger.PrintFatal(err, nil) } - logger := jsonlog.New(os.Stdout, jsonlog.LevelInfo) - - db, err := database.New(cfg.db.dsn, cfg.db.maxOpenConns, cfg.db.maxIdleConns, cfg.db.maxIdleTime, true) + db, err := database.New(cfg.DB.Dsn, cfg.DB.MaxOpenConns, cfg.DB.MaxIdleConns, cfg.DB.MaxIdleTime, true) if err != nil { logger.PrintFatal(err, nil) } defer db.Close() logger.PrintInfo("database connection pool established", nil) - expvar.NewString("version").Set(version) + expvar.NewString("version").Set(config.Version) expvar.Publish("goroutines", expvar.Func(func() interface{} { return runtime.NumGoroutine() @@ -124,7 +62,7 @@ func main() { config: cfg, logger: logger, models: database.NewModels(db), - mailer: mailer.New(cfg.smtp.host, cfg.smtp.port, cfg.smtp.username, cfg.smtp.password, cfg.smtp.sender), + mailer: mailer.New(cfg.Smtp.Host, cfg.Smtp.Port, cfg.Smtp.Username, cfg.Smtp.Password, cfg.Smtp.Sender), } err = app.serve(db) diff --git a/cmd/api/middleware.go b/cmd/api/middleware.go index 283e3d1..263bd7e 100644 --- a/cmd/api/middleware.go +++ b/cmd/api/middleware.go @@ -69,7 +69,7 @@ func (app *application) authenticate(next http.Handler) http.Handler { } token := headerParts[1] - claims, err := jwt.HMACCheck([]byte(token), []byte(app.config.jwt.secret)) + claims, err := jwt.HMACCheck([]byte(token), []byte(app.config.Jwt.Secret)) if err != nil { app.invalidAuthenticationTokenResponse(writer, request) return @@ -157,9 +157,9 @@ func (app *application) enableCORS(next http.Handler) http.Handler { origin := request.Header.Get("Origin") - if origin != "" && len(app.config.cors.trustedOrigins) != 0 { - for i := range app.config.cors.trustedOrigins { - if origin == app.config.cors.trustedOrigins[i] { + if origin != "" && len(app.config.Cors.TrustedOrigins) != 0 { + for i := range app.config.Cors.TrustedOrigins { + if origin == app.config.Cors.TrustedOrigins[i] { writer.Header().Set("Access-Control-Allow-Origin", origin) if request.Method == http.MethodOptions && request.Header.Get("Access-Control-Request-Method") != "" { diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 3424644..c2615f5 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -28,7 +28,7 @@ func (app *application) routes(db *sql.DB) http.Handler { router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requirePermission("movies:write", app.updateMovieHandler)) router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requirePermission("movies:write", app.deleteMovieHandler)) - _authService.RegisterHandlers(_authApp.NewApp(_authRepo.NewUserRepo(db), _authRepo.NewTokenRepo(db)), router) + _authService.RegisterHandlers(_authApp.NewApp(_authRepo.NewUserRepo(db), _authRepo.NewTokenRepo(db), app.logger, &app.wg, app.config), router) router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler) router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler) diff --git a/cmd/api/server.go b/cmd/api/server.go index 6296378..0d4fba5 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -14,7 +14,7 @@ import ( func (app *application) serve(db *sql.DB) error { srv := &http.Server{ - Addr: fmt.Sprintf(":%d", app.config.port), + Addr: fmt.Sprintf(":%d", app.config.Port), Handler: app.routes(db), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, @@ -50,7 +50,7 @@ func (app *application) serve(db *sql.DB) error { app.logger.PrintInfo("starting server", map[string]string{ "addr": srv.Addr, - "env": app.config.env, + "env": app.config.Env, }) err := srv.ListenAndServe() diff --git a/cmd/api/tokens.go b/cmd/api/tokens.go index 74c2531..c550834 100644 --- a/cmd/api/tokens.go +++ b/cmd/api/tokens.go @@ -73,7 +73,7 @@ func (app *application) createAuthenticationTokenHandler(writer http.ResponseWri claims.Audiences = []string{"greenlight.tarralva.com"} claims.Audiences = []string{"greenlight.tarralva.com"} - jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.jwt.secret)) + jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.Jwt.Secret)) if err != nil { app.serverErrorResponse(writer, request, err) return diff --git a/cmd/api/users.go b/cmd/api/users.go index 5982d99..8fd3783 100644 --- a/cmd/api/users.go +++ b/cmd/api/users.go @@ -7,12 +7,6 @@ import ( "net/http" ) -type createUserRequest struct { - Name string `json:"name"` - Email string `json:"email"` - Password string `json:"password"` -} - // @Summary Activate User // @Description Activates a user account using a token that was previously sent when successfully register a new user // @Tags Users diff --git a/internal/config/config.go b/internal/config/config.go index 603d3b4..954617e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,68 +9,66 @@ import ( ) var ( - version string + Version string port string env string ) type Config struct { - port int - env string - db struct { - dsn string - maxOpenConns int - maxIdleConns int - maxIdleTime string + Port int + Env string + DB struct { + Dsn string + MaxOpenConns int + MaxIdleConns int + MaxIdleTime string } - smtp struct { - host string - port int - username string - password string - sender string + Smtp struct { + Host string + Port int + Username string + Password string + Sender string } - cors struct { - trustedOrigins []string + Cors struct { + TrustedOrigins []string } - jwt struct { - secret string + Jwt struct { + Secret string } } -func Init() (cfg *Config, err error) { - cfg = &Config{} - +func Init() (cfg Config, err error) { intPort, _ := strconv.Atoi(port) - flag.IntVar(&cfg.port, "port", intPort, "API server port") - flag.StringVar(&cfg.env, "env", env, "Environment (development|staging|production)") - flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv( + flag.IntVar(&cfg.Port, "port", intPort, "API server port") + flag.StringVar(&cfg.Env, "env", env, "Environment (development|staging|production)") + flag.StringVar(&cfg.DB.Dsn, "db-dsn", os.Getenv( "DATABASE_URL"), "PostgreSQL DSN") - flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") - flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") - flag.StringVar(&cfg.db.maxIdleTime, "db-max-idle-time", "15m", "PostgreSQL max connection idle time") + flag.IntVar(&cfg.DB.MaxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") + flag.IntVar(&cfg.DB.MaxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") + flag.StringVar(&cfg.DB.MaxIdleTime, "db-max-idle-time", "15m", "PostgreSQL max connection idle time") smtpPort, _ := strconv.Atoi(os.Getenv("SMTP_PORT")) - flag.StringVar(&cfg.smtp.host, "smtp-host", os.Getenv("SMTP_HOST"), "SMTP host") - flag.IntVar(&cfg.smtp.port, "smtp-port", smtpPort, "SMTP port") - flag.StringVar(&cfg.smtp.username, "smtp-username", os.Getenv("SMTP_USERNAME"), "SMTP username") - flag.StringVar(&cfg.smtp.password, "smtp-password", os.Getenv("SMTP_PASSWORD"), "SMTP password") - flag.StringVar(&cfg.smtp.sender, "smtp-sender", os.Getenv("SMTP_SENDER"), "SMTP sender") + flag.StringVar(&cfg.Smtp.Host, "smtp-host", os.Getenv("SMTP_HOST"), "SMTP host") + flag.IntVar(&cfg.Smtp.Port, "smtp-port", smtpPort, "SMTP port") + flag.StringVar(&cfg.Smtp.Username, "smtp-username", os.Getenv("SMTP_USERNAME"), "SMTP username") + flag.StringVar(&cfg.Smtp.Password, "smtp-password", os.Getenv("SMTP_PASSWORD"), "SMTP password") + flag.StringVar(&cfg.Smtp.Sender, "smtp-sender", os.Getenv("SMTP_SENDER"), "SMTP sender") flag.Func("cors-trusted-origins", "Trusted CORS origins (space separated)", func(val string) error { - cfg.cors.trustedOrigins = strings.Fields(val) + cfg.Cors.TrustedOrigins = strings.Fields(val) return nil }) - flag.StringVar(&cfg.jwt.secret, "jwt-secret", os.Getenv("JWT_SECRET"), "JWT secret") + flag.StringVar(&cfg.Jwt.Secret, "jwt-secret", os.Getenv("JWT_SECRET"), "JWT secret") displayVersion := flag.Bool("version", false, "Display version and exit") flag.Parse() if *displayVersion { - fmt.Printf("Version:\t%s\n", version) + fmt.Printf("Version:\t%s\n", Version) } return cfg, nil diff --git a/internal/errors/errors.go b/internal/errors/errors.go index fca1472..c30ded7 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -1,9 +1,7 @@ package errors import ( - "fmt" "github.com/jessicatarra/greenlight/internal/response" - "github.com/jessicatarra/greenlight/internal/validator" "log/slog" "net/http" "strings" @@ -35,42 +33,42 @@ func ServerError(w http.ResponseWriter, r *http.Request, err error) { errorMessage(w, r, http.StatusInternalServerError, message, nil) } -func notFound(w http.ResponseWriter, r *http.Request) { - message := "The requested resource could not be found" - errorMessage(w, r, http.StatusNotFound, message, nil) -} +//func notFound(w http.ResponseWriter, r *http.Request) { +// message := "The requested resource could not be found" +// errorMessage(w, r, http.StatusNotFound, message, nil) +//} -func methodNotAllowed(w http.ResponseWriter, r *http.Request) { - message := fmt.Sprintf("The %s method is not supported for this resource", r.Method) - errorMessage(w, r, http.StatusMethodNotAllowed, message, nil) -} +//func methodNotAllowed(w http.ResponseWriter, r *http.Request) { +// message := fmt.Sprintf("The %s method is not supported for this resource", r.Method) +// errorMessage(w, r, http.StatusMethodNotAllowed, message, nil) +//} func BadRequest(w http.ResponseWriter, r *http.Request, err error) { errorMessage(w, r, http.StatusBadRequest, err.Error(), nil) } -func FailedValidation(w http.ResponseWriter, r *http.Request, v validator.Validator) { - err := response.JSON(w, http.StatusUnprocessableEntity, v) - if err != nil { - ServerError(w, r, err) - } -} - -func invalidAuthenticationToken(w http.ResponseWriter, r *http.Request) { - headers := make(http.Header) - headers.Set("WWW-Authenticate", "Bearer") - - errorMessage(w, r, http.StatusUnauthorized, "Invalid authentication token", headers) -} - -func authenticationRequired(w http.ResponseWriter, r *http.Request) { - errorMessage(w, r, http.StatusUnauthorized, "You must be authenticated to access this resource", nil) -} - -func basicAuthenticationRequired(w http.ResponseWriter, r *http.Request) { - headers := make(http.Header) - headers.Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) - - message := "You must be authenticated to access this resource" - errorMessage(w, r, http.StatusUnauthorized, message, headers) -} +//func FailedValidation(w http.ResponseWriter, r *http.Request, v validator.Validator) { +// err := response.JSON(w, http.StatusUnprocessableEntity, v) +// if err != nil { +// ServerError(w, r, err) +// } +//} +// +//func invalidAuthenticationToken(w http.ResponseWriter, r *http.Request) { +// headers := make(http.Header) +// headers.Set("WWW-Authenticate", "Bearer") +// +// errorMessage(w, r, http.StatusUnauthorized, "Invalid authentication token", headers) +//} +// +//func authenticationRequired(w http.ResponseWriter, r *http.Request) { +// errorMessage(w, r, http.StatusUnauthorized, "You must be authenticated to access this resource", nil) +//} +// +//func basicAuthenticationRequired(w http.ResponseWriter, r *http.Request) { +// headers := make(http.Header) +// headers.Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) +// +// message := "You must be authenticated to access this resource" +// errorMessage(w, r, http.StatusUnauthorized, message, headers) +//} diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go new file mode 100644 index 0000000..1d24200 --- /dev/null +++ b/internal/helpers/helpers.go @@ -0,0 +1,38 @@ +package helpers + +import ( + "fmt" + "github.com/jessicatarra/greenlight/internal/jsonlog" + "sync" +) + +type Resource interface { + Background(fn func()) +} + +type resource struct { + wg *sync.WaitGroup + logger *jsonlog.Logger +} + +func NewBackgroundTask(wg *sync.WaitGroup, logger *jsonlog.Logger) Resource { + return &resource{ + wg: wg, + logger: logger, + } +} + +func (r *resource) Background(fn func()) { + r.wg.Add(1) + + go func() { + defer r.wg.Done() + defer func() { + if err := recover(); err != nil { + r.logger.PrintError(fmt.Errorf("%s", err), nil) + } + }() + + fn() + }() +} diff --git a/ms/auth/app/app.go b/ms/auth/app/app.go index 4b0d66f..96fbd47 100644 --- a/ms/auth/app/app.go +++ b/ms/auth/app/app.go @@ -1,9 +1,14 @@ package app import ( + "github.com/jessicatarra/greenlight/internal/config" + "github.com/jessicatarra/greenlight/internal/helpers" + "github.com/jessicatarra/greenlight/internal/jsonlog" + "github.com/jessicatarra/greenlight/internal/mailer" "github.com/jessicatarra/greenlight/internal/validator" "github.com/jessicatarra/greenlight/ms/auth/entity" "github.com/jessicatarra/greenlight/ms/auth/repositories" + "sync" "time" ) @@ -40,12 +45,21 @@ type App interface { type app struct { userRepo repositories.UserRepository tokenRepo repositories.TokenRepository + helpers helpers.Resource + logger *jsonlog.Logger + wg *sync.WaitGroup + mailer mailer.Mailer } -func NewApp(userRepo repositories.UserRepository, tokenRepo repositories.TokenRepository) App { +func NewApp(userRepo repositories.UserRepository, tokenRepo repositories.TokenRepository, logger *jsonlog.Logger, + wg *sync.WaitGroup, cfg config.Config) App { return &app{ userRepo: userRepo, tokenRepo: tokenRepo, + helpers: helpers.NewBackgroundTask(wg, logger), + logger: logger, + wg: wg, + mailer: mailer.New(cfg.Smtp.Host, cfg.Smtp.Port, cfg.Smtp.Username, cfg.Smtp.Password, cfg.Smtp.Sender), } } @@ -72,12 +86,25 @@ func (a *app) Create(input CreateUserRequest) (*entity.User, error) { return nil, err } - _, err = a.tokenRepo.New(user.ID, 3*24*time.Hour, repositories.ScopeActivation) + token, err := a.tokenRepo.New(user.ID, 3*24*time.Hour, repositories.ScopeActivation) if err != nil { return nil, err } - //print(token.Plaintext) + fn := func() { + data := map[string]interface{}{ + "activationToken": token.Plaintext, + "userID": user.ID, + } + print(token.Plaintext) + + err = a.mailer.Send(user.Email, "user_welcome.gohtml", data) + if err != nil { + a.logger.PrintError(err, nil) + } + } + + a.helpers.Background(fn) return user, err } diff --git a/ms/auth/app/app_test.go b/ms/auth/app/app_test.go index 5d6bfc1..ebff67d 100644 --- a/ms/auth/app/app_test.go +++ b/ms/auth/app/app_test.go @@ -1,11 +1,14 @@ package app import ( + "github.com/jessicatarra/greenlight/internal/config" + "github.com/jessicatarra/greenlight/internal/jsonlog" "github.com/jessicatarra/greenlight/internal/validator" "github.com/jessicatarra/greenlight/ms/auth/entity" "github.com/jessicatarra/greenlight/ms/auth/repositories/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "sync" "testing" ) @@ -48,9 +51,26 @@ func TestCreate(t *testing.T) { // Initialize the repositories mock userRepo := mocks.UserRepository{} tokenRepo := mocks.TokenRepository{} + logger := &jsonlog.Logger{} + wg := &sync.WaitGroup{} + cfg := config.Config{ + Smtp: struct { + Host string + Port int + Username string + Password string + Sender string + }{ + Host: "sandbox.smtp.mailtrap.io", + Port: 25, + Username: "username", + Password: "password", + Sender: "Greenlight ", + }, + } // Create the app instance with the repositories mock - app := NewApp(&userRepo, &tokenRepo) + app := NewApp(&userRepo, &tokenRepo, logger, wg, cfg) // Prepare the input for the Create function input := CreateUserRequest{ @@ -59,7 +79,6 @@ func TestCreate(t *testing.T) { Password: "password123", } - // Set the expectations on the user repositories mock userRepo.On("InsertNewUser", mock.AnythingOfType("*entity.User")).Return(nil) tokenRepo.On("New", mock.Anything, mock.AnythingOfType("time.Duration"), mock.IsType("string")).Return(nil, nil)