diff --git a/cmd/gophermart/main.go b/cmd/gophermart/main.go index f9130c7..586a1d3 100644 --- a/cmd/gophermart/main.go +++ b/cmd/gophermart/main.go @@ -13,18 +13,21 @@ import ( func main() { mainCtx := context.Background() config.Init() - logger.Init() - db.Init() + loggerRes := logger.NewLogger() + dbRes := db.NewDB(loggerRes) orderQueue := make(chan string) defer close(orderQueue) - userService := services.NewUserService() - orderService := services.NewOrderService(orderQueue) - withdrawalService := services.NewWithdrawalService() + userService := services.NewUserService(dbRes) + orderService := services.NewOrderService(orderQueue, dbRes, loggerRes) + withdrawalService := services.NewWithdrawalService(dbRes, loggerRes) - orderWorker := workers.NewOrderWorker(mainCtx, orderService, orderQueue) + orderWorker := workers.NewOrderWorker(mainCtx, orderService, orderQueue, loggerRes) orderWorker.Run() - server.Start(userService, orderService, withdrawalService) + appServer := server.NewServer(userService, orderService, withdrawalService, loggerRes) + if err := appServer.Start(); err != nil { + loggerRes.Fatal(err.Error()) + } } diff --git a/internal/authenticate/authenticate.go b/internal/authenticate/authenticate.go index 1614641..92dc66a 100644 --- a/internal/authenticate/authenticate.go +++ b/internal/authenticate/authenticate.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" - "github.com/spqrcor/gofermart/internal/logger" "net/http" "strings" "time" @@ -62,9 +61,7 @@ func GetUserIDFromCookie(tokenString string) (uuid.UUID, error) { func SetCookie(rw http.ResponseWriter, UserID uuid.UUID) { cookie, err := CreateCookie(UserID) - if err != nil { - logger.Log.Error(err.Error()) - } else { + if err == nil { http.SetCookie(rw, &cookie) } } diff --git a/internal/config/config.go b/internal/config/config.go index 92b559d..e6df4cb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" + "time" ) type Config struct { @@ -12,13 +13,17 @@ type Config struct { AccrualSystemAddress string `env:"ACCRUAL_SYSTEM_ADDRESS"` LogLevel zapcore.Level `env:"LOG_LEVEL"` DatabaseURI string `env:"DATABASE_URI"` + QueryTimeOut time.Duration `env:"QUERY_TIME_OUT"` + WorkerCount int `env:"WORKER_COUNT"` } var Cfg = Config{ RunAddr: "localhost:8080", LogLevel: zap.InfoLevel, AccrualSystemAddress: "", - DatabaseURI: "", + DatabaseURI: "postgres://postgres:Sp123456@localhost:5432/gofermart?sslmode=disable", + QueryTimeOut: 3, + WorkerCount: 3, } func Init() { diff --git a/internal/db/connector.go b/internal/db/connector.go index b64c38f..2126cbf 100644 --- a/internal/db/connector.go +++ b/internal/db/connector.go @@ -3,30 +3,34 @@ package db import ( "database/sql" _ "github.com/jackc/pgx/v5/stdlib" + "github.com/pressly/goose/v3" "github.com/spqrcor/gofermart/internal/config" - "github.com/spqrcor/gofermart/internal/logger" + "go.uber.org/zap" ) -var Source *sql.DB - -func connect() (*sql.DB, error) { +func connect(logger *zap.Logger) (*sql.DB, error) { db, err := sql.Open("pgx", config.Cfg.DatabaseURI) if err != nil { - logger.Log.Fatal(err.Error()) + logger.Fatal(err.Error()) return nil, err } if err := db.Ping(); err != nil { - logger.Log.Error(err.Error()) + logger.Error(err.Error()) return nil, err } return db, nil } -func Init() { - res, err := connect() +func NewDB(logger *zap.Logger) *sql.DB { + res, err := connect(logger) if err != nil { - logger.Log.Fatal(err.Error()) + logger.Fatal(err.Error()) + } + if err := goose.SetDialect("postgres"); err != nil { + logger.Fatal(err.Error()) + } + if err := goose.Up(res, "internal/migrations"); err != nil { + logger.Fatal(err.Error()) } - Migrate(res) - Source = res + return res } diff --git a/internal/db/migrator.go b/internal/db/migrator.go deleted file mode 100644 index 124b1f6..0000000 --- a/internal/db/migrator.go +++ /dev/null @@ -1,17 +0,0 @@ -package db - -import ( - "database/sql" - "github.com/pressly/goose/v3" - "github.com/spqrcor/gofermart/internal/logger" -) - -func Migrate(db *sql.DB) { - if err := goose.SetDialect("postgres"); err != nil { - logger.Log.Fatal(err.Error()) - } - - if err := goose.Up(db, "internal/migrations"); err != nil { - logger.Log.Fatal(err.Error()) - } -} diff --git a/internal/handlers/add_orders.go b/internal/handlers/add_orders.go new file mode 100644 index 0000000..7510195 --- /dev/null +++ b/internal/handlers/add_orders.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "errors" + "github.com/spqrcor/gofermart/internal/services" + "github.com/spqrcor/gofermart/internal/utils" + "net/http" +) + +func AddOrdersHandler(o services.OrderRepository) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + orderNum, err := utils.FromPostPlain(req) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + + err = o.Add(req.Context(), orderNum) + if errors.Is(err, services.ErrOrderInvalidFormat) { + http.Error(res, err.Error(), http.StatusUnprocessableEntity) + return + } else if errors.Is(err, services.ErrOrderAnotherUserExists) { + res.WriteHeader(http.StatusConflict) + return + } else if errors.Is(err, services.ErrOrderUserExists) { + res.WriteHeader(http.StatusOK) + return + } else if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.WriteHeader(http.StatusAccepted) + } +} diff --git a/internal/handlers/add_withdrawals.go b/internal/handlers/add_withdrawals.go new file mode 100644 index 0000000..7938163 --- /dev/null +++ b/internal/handlers/add_withdrawals.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "errors" + "github.com/spqrcor/gofermart/internal/services" + "github.com/spqrcor/gofermart/internal/utils" + "net/http" +) + +func AddWithdrawalHandler(w services.WithdrawalRepository) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + var input services.InputWithdrawal + if err := utils.FromPostJSON(req, &input); err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + + err := w.Add(req.Context(), input) + if errors.Is(err, services.ErrOrderInvalidFormat) { + http.Error(res, err.Error(), http.StatusUnprocessableEntity) + return + } else if errors.Is(err, services.ErrBalance) { + http.Error(res, err.Error(), http.StatusPaymentRequired) + return + } else if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + + res.WriteHeader(http.StatusOK) + } +} diff --git a/internal/handlers/get_balance.go b/internal/handlers/get_balance.go new file mode 100644 index 0000000..e140090 --- /dev/null +++ b/internal/handlers/get_balance.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "encoding/json" + "github.com/spqrcor/gofermart/internal/services" + "net/http" +) + +func GetBalanceHandler(w services.WithdrawalRepository) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + balance, err := w.GetBalance(req.Context()) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + res.Header().Set("Content-Type", "application/json; charset=utf-8") + res.WriteHeader(http.StatusOK) + enc := json.NewEncoder(res) + if err := enc.Encode(balance); err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + } +} diff --git a/internal/handlers/get_orders.go b/internal/handlers/get_orders.go new file mode 100644 index 0000000..71f9d7f --- /dev/null +++ b/internal/handlers/get_orders.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "encoding/json" + "errors" + "github.com/spqrcor/gofermart/internal/services" + "net/http" +) + +func GetOrdersHandler(o services.OrderRepository) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + orders, err := o.GetAll(req.Context()) + if errors.Is(err, services.ErrOrdersNotFound) { + res.WriteHeader(http.StatusNoContent) + return + } else if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + res.Header().Set("Content-Type", "application/json; charset=utf-8") + res.WriteHeader(http.StatusOK) + enc := json.NewEncoder(res) + if err := enc.Encode(orders); err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + } +} diff --git a/internal/handlers/get_withdrawals.go b/internal/handlers/get_withdrawals.go new file mode 100644 index 0000000..017f0b3 --- /dev/null +++ b/internal/handlers/get_withdrawals.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "encoding/json" + "errors" + "github.com/spqrcor/gofermart/internal/services" + "net/http" +) + +func GetWithdrawalsHandler(w services.WithdrawalRepository) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + withdrawals, err := w.GetAll(req.Context()) + if errors.Is(err, services.ErrWithdrawNotFound) { + res.WriteHeader(http.StatusNoContent) + return + } else if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + res.Header().Set("Content-Type", "application/json; charset=utf-8") + res.WriteHeader(http.StatusOK) + enc := json.NewEncoder(res) + if err := enc.Encode(withdrawals); err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + } +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go deleted file mode 100644 index 20e716e..0000000 --- a/internal/handlers/handlers.go +++ /dev/null @@ -1,167 +0,0 @@ -package handlers - -import ( - "encoding/json" - "errors" - "github.com/spqrcor/gofermart/internal/authenticate" - "github.com/spqrcor/gofermart/internal/services" - "github.com/spqrcor/gofermart/internal/utils" - "net/http" -) - -func RegisterHandler(u services.UserRepository) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - var input services.InputDataUser - if err := utils.FromPostJSON(req, &input); err != nil { - res.WriteHeader(http.StatusBadRequest) - return - } - - UserID, err := u.Add(req.Context(), input) - if errors.Is(err, services.ErrValidation) { - http.Error(res, err.Error(), http.StatusBadRequest) - return - } else if errors.Is(err, services.ErrLoginExists) { - res.WriteHeader(http.StatusConflict) - return - } else if err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - - authenticate.SetCookie(res, UserID) - res.WriteHeader(http.StatusOK) - } -} - -func LoginHandler(u services.UserRepository) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - var input services.InputDataUser - if err := utils.FromPostJSON(req, &input); err != nil { - res.WriteHeader(http.StatusBadRequest) - return - } - - UserID, err := u.Login(req.Context(), input) - if errors.Is(err, services.ErrLogin) { - http.Error(res, err.Error(), http.StatusUnauthorized) - return - } else if err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - - authenticate.SetCookie(res, UserID) - res.WriteHeader(http.StatusOK) - } -} - -func AddOrdersHandler(o services.OrderRepository) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - orderNum, err := utils.FromPostPlain(req) - if err != nil { - res.WriteHeader(http.StatusBadRequest) - return - } - - err = o.Add(req.Context(), orderNum) - if errors.Is(err, services.ErrOrderInvalidFormat) { - http.Error(res, err.Error(), http.StatusUnprocessableEntity) - return - } else if errors.Is(err, services.ErrOrderAnotherUserExists) { - res.WriteHeader(http.StatusConflict) - return - } else if errors.Is(err, services.ErrOrderUserExists) { - res.WriteHeader(http.StatusOK) - return - } else if err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - res.WriteHeader(http.StatusAccepted) - } -} - -func GetOrdersHandler(o services.OrderRepository) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - orders, err := o.GetAll(req.Context()) - if errors.Is(err, services.ErrOrdersNotFound) { - res.WriteHeader(http.StatusNoContent) - return - } else if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - - res.Header().Set("Content-Type", "application/json; charset=utf-8") - res.WriteHeader(http.StatusOK) - enc := json.NewEncoder(res) - if err := enc.Encode(orders); err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - } -} - -func GetBalanceHandler(w services.WithdrawalRepository) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - balance, err := w.GetBalance(req.Context()) - if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - - res.Header().Set("Content-Type", "application/json; charset=utf-8") - res.WriteHeader(http.StatusOK) - enc := json.NewEncoder(res) - if err := enc.Encode(balance); err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - } -} - -func GetWithdrawalsHandler(w services.WithdrawalRepository) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - withdrawals, err := w.GetAll(req.Context()) - if errors.Is(err, services.ErrWithdrawNotFound) { - res.WriteHeader(http.StatusNoContent) - return - } else if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - - res.Header().Set("Content-Type", "application/json; charset=utf-8") - res.WriteHeader(http.StatusOK) - enc := json.NewEncoder(res) - if err := enc.Encode(withdrawals); err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - } -} - -func AddWithdrawalHandler(w services.WithdrawalRepository) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - var input services.InputWithdrawal - if err := utils.FromPostJSON(req, &input); err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - - err := w.Add(req.Context(), input) - if errors.Is(err, services.ErrOrderInvalidFormat) { - http.Error(res, err.Error(), http.StatusUnprocessableEntity) - return - } else if errors.Is(err, services.ErrBalance) { - http.Error(res, err.Error(), http.StatusPaymentRequired) - return - } else if err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - - res.WriteHeader(http.StatusOK) - } -} diff --git a/internal/handlers/login.go b/internal/handlers/login.go new file mode 100644 index 0000000..b65aabf --- /dev/null +++ b/internal/handlers/login.go @@ -0,0 +1,31 @@ +package handlers + +import ( + "errors" + "github.com/spqrcor/gofermart/internal/authenticate" + "github.com/spqrcor/gofermart/internal/services" + "github.com/spqrcor/gofermart/internal/utils" + "net/http" +) + +func LoginHandler(u services.UserRepository) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + var input services.InputDataUser + if err := utils.FromPostJSON(req, &input); err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + + UserID, err := u.Login(req.Context(), input) + if errors.Is(err, services.ErrLogin) { + http.Error(res, err.Error(), http.StatusUnauthorized) + return + } else if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + + authenticate.SetCookie(res, UserID) + res.WriteHeader(http.StatusOK) + } +} diff --git a/internal/handlers/register.go b/internal/handlers/register.go new file mode 100644 index 0000000..5843620 --- /dev/null +++ b/internal/handlers/register.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "errors" + "github.com/spqrcor/gofermart/internal/authenticate" + "github.com/spqrcor/gofermart/internal/services" + "github.com/spqrcor/gofermart/internal/utils" + "net/http" +) + +func RegisterHandler(u services.UserRepository) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + var input services.InputDataUser + if err := utils.FromPostJSON(req, &input); err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + + UserID, err := u.Add(req.Context(), input) + if errors.Is(err, services.ErrValidation) { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } else if errors.Is(err, services.ErrLoginExists) { + res.WriteHeader(http.StatusConflict) + return + } else if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + + authenticate.SetCookie(res, UserID) + res.WriteHeader(http.StatusOK) + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 3ebf795..b38ca76 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -8,12 +8,12 @@ import ( var Log *zap.Logger = zap.NewNop() -func Init() { +func NewLogger() *zap.Logger { cfg := zap.NewProductionConfig() cfg.Level = zap.NewAtomicLevelAt(config.Cfg.LogLevel) zl, err := cfg.Build() if err != nil { log.Fatal(err) } - Log = zl + return zl } diff --git a/internal/server/authenticate.go b/internal/server/authenticate.go new file mode 100644 index 0000000..11482ca --- /dev/null +++ b/internal/server/authenticate.go @@ -0,0 +1,29 @@ +package server + +import ( + "context" + "github.com/spqrcor/gofermart/internal/authenticate" + "go.uber.org/zap" + "net/http" +) + +func authenticateMiddleware(logger *zap.Logger) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("Authorization") + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } else { + UserID, err := authenticate.GetUserIDFromCookie(cookie.Value) + if err != nil { + logger.Error(err.Error()) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + ctx := context.WithValue(r.Context(), authenticate.ContextUserID, UserID) + next.ServeHTTP(rw, r.WithContext(ctx)) + } + }) + } +} diff --git a/internal/server/gzip.go b/internal/server/gzip.go new file mode 100644 index 0000000..9235e23 --- /dev/null +++ b/internal/server/gzip.go @@ -0,0 +1,26 @@ +package server + +import ( + "compress/gzip" + "go.uber.org/zap" + "net/http" +) + +func getBodyMiddleware(logger *zap.Logger) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.Header.Get(`Content-Encoding`) == `gzip` { + gz, err := gzip.NewReader(r.Body) + if err != nil { + logger.Error(err.Error()) + } else { + r.Body = gz + } + if err = gz.Close(); err != nil { + logger.Error(err.Error()) + } + } + next.ServeHTTP(rw, r) + }) + } +} diff --git a/internal/server/logger.go b/internal/server/logger.go new file mode 100644 index 0000000..81a64f0 --- /dev/null +++ b/internal/server/logger.go @@ -0,0 +1,27 @@ +package server + +import ( + "github.com/go-chi/chi/v5/middleware" + "go.uber.org/zap" + "net/http" + "time" +) + +func loggerMiddleware(logger *zap.Logger) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + start := time.Now() + ww := middleware.NewWrapResponseWriter(rw, r.ProtoMajor) + next.ServeHTTP(ww, r) + duration := time.Since(start) + + logger.Info("HTTP request", + zap.String("method", r.Method), + zap.String("path", r.URL.Path), + zap.Int("status", ww.Status()), + zap.Int("content-length", ww.BytesWritten()), + zap.String("duration", duration.String()), + ) + }) + } +} diff --git a/internal/server/middleware.go b/internal/server/middleware.go deleted file mode 100644 index 1f1abec..0000000 --- a/internal/server/middleware.go +++ /dev/null @@ -1,70 +0,0 @@ -package server - -import ( - "compress/gzip" - "context" - "github.com/go-chi/chi/v5/middleware" - "github.com/spqrcor/gofermart/internal/authenticate" - "github.com/spqrcor/gofermart/internal/logger" - "go.uber.org/zap" - "net/http" - "slices" - "time" -) - -func getBodyMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if r.Header.Get(`Content-Encoding`) == `gzip` { - gz, err := gzip.NewReader(r.Body) - if err != nil { - logger.Log.Error(err.Error()) - } else { - r.Body = gz - } - if err = gz.Close(); err != nil { - logger.Log.Error(err.Error()) - } - } - next.ServeHTTP(rw, r) - }) -} - -func loggerMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - start := time.Now() - ww := middleware.NewWrapResponseWriter(rw, r.ProtoMajor) - next.ServeHTTP(ww, r) - duration := time.Since(start) - - logger.Log.Info("HTTP request", - zap.String("method", r.Method), - zap.String("path", r.URL.Path), - zap.Int("status", ww.Status()), - zap.Int("content-length", ww.BytesWritten()), - zap.String("duration", duration.String()), - ) - }) -} - -func authenticateMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("Authorization") - if err != nil { - idx := slices.IndexFunc(publicRoutes, func(c string) bool { return c == r.RequestURI }) - if idx == -1 { - http.Error(rw, err.Error(), http.StatusUnauthorized) - return - } - next.ServeHTTP(rw, r) - } else { - UserID, err := authenticate.GetUserIDFromCookie(cookie.Value) - if err != nil { - logger.Log.Error(err.Error()) - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - ctx := context.WithValue(r.Context(), authenticate.ContextUserID, UserID) - next.ServeHTTP(rw, r.WithContext(ctx)) - } - }) -} diff --git a/internal/server/server.go b/internal/server/server.go index 23fe866..951ca24 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -6,29 +6,45 @@ import ( "github.com/spqrcor/gofermart/internal/config" "github.com/spqrcor/gofermart/internal/handlers" "github.com/spqrcor/gofermart/internal/services" - "log" + "go.uber.org/zap" "net/http" ) -var publicRoutes = []string{"/api/user/register", "/api/user/login"} +type HTTPServer struct { + userService services.UserRepository + orderService services.OrderRepository + withdrawalService services.WithdrawalRepository + logger *zap.Logger +} + +func NewServer(userService services.UserRepository, orderService services.OrderRepository, withdrawalService services.WithdrawalRepository, logger *zap.Logger) *HTTPServer { + return &HTTPServer{ + userService: userService, orderService: orderService, withdrawalService: withdrawalService, logger: logger, + } +} -func Start(userService services.UserRepository, orderService services.OrderRepository, withdrawalService services.WithdrawalRepository) { +func (s *HTTPServer) Start() error { r := chi.NewRouter() - r.Use(authenticateMiddleware) - r.Use(loggerMiddleware) + r.Use(loggerMiddleware(s.logger)) r.Use(middleware.Compress(5, "application/json", "text/html")) - r.Use(getBodyMiddleware) + r.Use(getBodyMiddleware(s.logger)) - r.Post("/api/user/register", handlers.RegisterHandler(userService)) - r.Post("/api/user/login", handlers.LoginHandler(userService)) - r.Post("/api/user/orders", handlers.AddOrdersHandler(orderService)) - r.Get("/api/user/orders", handlers.GetOrdersHandler(orderService)) - r.Get("/api/user/balance", handlers.GetBalanceHandler(withdrawalService)) - r.Post("/api/user/balance/withdraw", handlers.AddWithdrawalHandler(withdrawalService)) - r.Get("/api/user/withdrawals", handlers.GetWithdrawalsHandler(withdrawalService)) + r.Group(func(r chi.Router) { + r.Post("/api/user/register", handlers.RegisterHandler(s.userService)) + r.Post("/api/user/login", handlers.LoginHandler(s.userService)) + }) + + r.Group(func(r chi.Router) { + r.Use(authenticateMiddleware(s.logger)) + r.Post("/api/user/orders", handlers.AddOrdersHandler(s.orderService)) + r.Get("/api/user/orders", handlers.GetOrdersHandler(s.orderService)) + r.Get("/api/user/balance", handlers.GetBalanceHandler(s.withdrawalService)) + r.Post("/api/user/balance/withdraw", handlers.AddWithdrawalHandler(s.withdrawalService)) + r.Get("/api/user/withdrawals", handlers.GetWithdrawalsHandler(s.withdrawalService)) + }) r.HandleFunc(`/*`, func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusBadRequest) }) - log.Fatal(http.ListenAndServe(config.Cfg.RunAddr, r)) + return http.ListenAndServe(config.Cfg.RunAddr, r) } diff --git a/internal/services/order.go b/internal/services/order.go index 3be75c9..63448a9 100644 --- a/internal/services/order.go +++ b/internal/services/order.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/google/uuid" "github.com/spqrcor/gofermart/internal/authenticate" - "github.com/spqrcor/gofermart/internal/db" - "github.com/spqrcor/gofermart/internal/logger" + "github.com/spqrcor/gofermart/internal/config" "github.com/spqrcor/gofermart/internal/utils" + "go.uber.org/zap" "time" ) @@ -39,10 +39,12 @@ type OrderRepository interface { type OrderService struct { orderQueue chan string + db *sql.DB + logger *zap.Logger } -func NewOrderService(orderQueue chan string) *OrderService { - return &OrderService{orderQueue: orderQueue} +func NewOrderService(orderQueue chan string, db *sql.DB, logger *zap.Logger) *OrderService { + return &OrderService{orderQueue: orderQueue, db: db, logger: logger} } func (o *OrderService) Add(ctx context.Context, orderNum string) error { @@ -52,9 +54,9 @@ func (o *OrderService) Add(ctx context.Context, orderNum string) error { var baseUserID, baseOrderID string orderID := uuid.NewString() - childCtx, cancel := context.WithTimeout(ctx, time.Second*3) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - err := db.Source.QueryRowContext(childCtx, "INSERT INTO orders (id, user_id, number) VALUES ($1, $2, $3) "+ + err := o.db.QueryRowContext(childCtx, "INSERT INTO orders (id, user_id, number) VALUES ($1, $2, $3) "+ "ON CONFLICT(number) DO UPDATE SET number = EXCLUDED.number RETURNING id, user_id", orderID, ctx.Value(authenticate.ContextUserID), orderNum).Scan(&baseOrderID, &baseUserID) if err != nil { return err @@ -69,19 +71,19 @@ func (o *OrderService) Add(ctx context.Context, orderNum string) error { func (o *OrderService) GetAll(ctx context.Context) ([]Order, error) { var orders []Order - childCtx, cancel := context.WithTimeout(ctx, time.Second*3) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - rows, err := db.Source.QueryContext(childCtx, "SELECT number, status, accrual, created_at FROM orders WHERE user_id = $1 ORDER BY created_at DESC", ctx.Value(authenticate.ContextUserID)) + rows, err := o.db.QueryContext(childCtx, "SELECT number, status, accrual, created_at FROM orders WHERE user_id = $1 ORDER BY created_at DESC", ctx.Value(authenticate.ContextUserID)) if err != nil { return nil, err } defer func() { if err := rows.Close(); err != nil { - logger.Log.Error(err.Error()) + o.logger.Error(err.Error()) } if err := rows.Err(); err != nil { - logger.Log.Error(err.Error()) + o.logger.Error(err.Error()) } }() @@ -104,19 +106,19 @@ func (o *OrderService) GetAll(ctx context.Context) ([]Order, error) { func (o *OrderService) GetUnComplete(ctx context.Context) ([]string, error) { var orders []string - childCtx, cancel := context.WithTimeout(ctx, time.Second*5) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - rows, err := db.Source.QueryContext(childCtx, "SELECT number FROM orders WHERE status IN ('NEW', 'PROCESSING') ORDER BY created_at") + rows, err := o.db.QueryContext(childCtx, "SELECT number FROM orders WHERE status IN ('NEW', 'PROCESSING') ORDER BY created_at") if err != nil { return nil, err } defer func() { if err := rows.Close(); err != nil { - logger.Log.Error(err.Error()) + o.logger.Error(err.Error()) } if err := rows.Err(); err != nil { - logger.Log.Error(err.Error()) + o.logger.Error(err.Error()) } }() @@ -131,10 +133,10 @@ func (o *OrderService) GetUnComplete(ctx context.Context) ([]string, error) { } func (o *OrderService) ChangeStatus(ctx context.Context, data OrderFromAccrual) error { - childCtx, cancel := context.WithTimeout(ctx, time.Second*3) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - tx, err := db.Source.BeginTx(childCtx, nil) + tx, err := o.db.BeginTx(childCtx, nil) if err != nil { return err } @@ -150,9 +152,5 @@ func (o *OrderService) ChangeStatus(ctx context.Context, data OrderFromAccrual) return err } } - - if err = tx.Commit(); err != nil { - return err - } - return nil + return tx.Commit() } diff --git a/internal/services/user.go b/internal/services/user.go index 1bbdebe..87f2705 100644 --- a/internal/services/user.go +++ b/internal/services/user.go @@ -6,7 +6,7 @@ import ( "errors" "fmt" "github.com/google/uuid" - "github.com/spqrcor/gofermart/internal/db" + "github.com/spqrcor/gofermart/internal/config" "golang.org/x/crypto/bcrypt" "time" ) @@ -30,10 +30,12 @@ type UserRepository interface { Login(ctx context.Context, input InputDataUser) (uuid.UUID, error) } -type UserService struct{} +type UserService struct { + db *sql.DB +} -func NewUserService() *UserService { - return &UserService{} +func NewUserService(db *sql.DB) *UserService { + return &UserService{db: db} } func (u *UserService) Add(ctx context.Context, input InputDataUser) (uuid.UUID, error) { @@ -47,9 +49,9 @@ func (u *UserService) Add(ctx context.Context, input InputDataUser) (uuid.UUID, baseUserID := "" userID := uuid.NewString() - childCtx, cancel := context.WithTimeout(ctx, time.Second*3) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - err = db.Source.QueryRowContext(childCtx, "INSERT INTO users (id, login, password) VALUES ($1, $2, $3) ON CONFLICT(login) DO UPDATE SET login = EXCLUDED.login RETURNING id", + err = u.db.QueryRowContext(childCtx, "INSERT INTO users (id, login, password) VALUES ($1, $2, $3) ON CONFLICT(login) DO UPDATE SET login = EXCLUDED.login RETURNING id", userID, input.Login, string(bytes)).Scan(&baseUserID) if err != nil { return uuid.Nil, err @@ -60,9 +62,9 @@ func (u *UserService) Add(ctx context.Context, input InputDataUser) (uuid.UUID, } func (u *UserService) Login(ctx context.Context, input InputDataUser) (uuid.UUID, error) { - childCtx, cancel := context.WithTimeout(ctx, time.Second*3) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - row := db.Source.QueryRowContext(childCtx, "SELECT id, password FROM users WHERE login = $1", input.Login) + row := u.db.QueryRowContext(childCtx, "SELECT id, password FROM users WHERE login = $1", input.Login) var userID, password string err := row.Scan(&userID, &password) diff --git a/internal/services/withdrawal.go b/internal/services/withdrawal.go index 76ac944..11715b8 100644 --- a/internal/services/withdrawal.go +++ b/internal/services/withdrawal.go @@ -7,9 +7,9 @@ import ( "fmt" "github.com/jackc/pgx/v5/pgconn" "github.com/spqrcor/gofermart/internal/authenticate" - "github.com/spqrcor/gofermart/internal/db" - "github.com/spqrcor/gofermart/internal/logger" + "github.com/spqrcor/gofermart/internal/config" "github.com/spqrcor/gofermart/internal/utils" + "go.uber.org/zap" "time" ) @@ -38,10 +38,13 @@ type WithdrawalRepository interface { GetBalance(ctx context.Context) (BalanceInfo, error) } -type WithdrawalService struct{} +type WithdrawalService struct { + db *sql.DB + logger *zap.Logger +} -func NewWithdrawalService() *WithdrawalService { - return &WithdrawalService{} +func NewWithdrawalService(db *sql.DB, logger *zap.Logger) *WithdrawalService { + return &WithdrawalService{db: db, logger: logger} } func (w *WithdrawalService) Add(ctx context.Context, input InputWithdrawal) error { @@ -50,10 +53,10 @@ func (w *WithdrawalService) Add(ctx context.Context, input InputWithdrawal) erro } userID := ctx.Value(authenticate.ContextUserID) - childCtx, cancel := context.WithTimeout(ctx, time.Second*3) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - tx, err := db.Source.BeginTx(childCtx, nil) + tx, err := w.db.BeginTx(childCtx, nil) if err != nil { return err } @@ -76,28 +79,25 @@ func (w *WithdrawalService) Add(ctx context.Context, input InputWithdrawal) erro } return err } - if err = tx.Commit(); err != nil { - return err - } - return nil + return tx.Commit() } func (w *WithdrawalService) GetAll(ctx context.Context) ([]Withdrawal, error) { var withdrawals []Withdrawal - childCtx, cancel := context.WithTimeout(ctx, time.Second*3) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - rows, err := db.Source.QueryContext(childCtx, "SELECT number, sum, created_at FROM withdrawals WHERE user_id = $1 ORDER BY created_at DESC", + rows, err := w.db.QueryContext(childCtx, "SELECT number, sum, created_at FROM withdrawals WHERE user_id = $1 ORDER BY created_at DESC", ctx.Value(authenticate.ContextUserID)) if err != nil { return nil, err } defer func() { if err := rows.Close(); err != nil { - logger.Log.Error(err.Error()) + w.logger.Error(err.Error()) } if err := rows.Err(); err != nil { - logger.Log.Error(err.Error()) + w.logger.Error(err.Error()) } }() @@ -116,10 +116,10 @@ func (w *WithdrawalService) GetAll(ctx context.Context) ([]Withdrawal, error) { func (w *WithdrawalService) GetBalance(ctx context.Context) (BalanceInfo, error) { balanceInfo := BalanceInfo{} - childCtx, cancel := context.WithTimeout(ctx, time.Second*3) + childCtx, cancel := context.WithTimeout(ctx, time.Second*config.Cfg.QueryTimeOut) defer cancel() - row := db.Source.QueryRowContext(childCtx, "SELECT balance, (SELECT COALESCE(SUM(w.sum), 0) FROM withdrawals w WHERE w.user_id = u.id) FROM users u WHERE id = $1", + row := w.db.QueryRowContext(childCtx, "SELECT balance, (SELECT COALESCE(SUM(w.sum), 0) FROM withdrawals w WHERE w.user_id = u.id) FROM users u WHERE id = $1", ctx.Value(authenticate.ContextUserID)) if err := row.Scan(&balanceInfo.Current, &balanceInfo.Withdrawn); err != nil { return balanceInfo, errors.New("user not found") diff --git a/internal/workers/order.go b/internal/workers/order.go index d1215a7..aef8607 100644 --- a/internal/workers/order.go +++ b/internal/workers/order.go @@ -3,26 +3,26 @@ package workers import ( "context" "github.com/spqrcor/gofermart/internal/client" - "github.com/spqrcor/gofermart/internal/logger" + "github.com/spqrcor/gofermart/internal/config" "github.com/spqrcor/gofermart/internal/services" + "go.uber.org/zap" "time" ) -const workerCount = 3 - type OrderWorker struct { ctx context.Context orderService services.OrderRepository orderQueue chan string + logger *zap.Logger } -func NewOrderWorker(ctx context.Context, orderService services.OrderRepository, orderQueue chan string) *OrderWorker { - return &OrderWorker{ctx: ctx, orderService: orderService, orderQueue: orderQueue} +func NewOrderWorker(ctx context.Context, orderService services.OrderRepository, orderQueue chan string, logger *zap.Logger) *OrderWorker { + return &OrderWorker{ctx: ctx, orderService: orderService, orderQueue: orderQueue, logger: logger} } func (w *OrderWorker) Run() { go w.fillQueue() - for i := 1; i <= workerCount; i++ { + for i := 1; i <= config.Cfg.WorkerCount; i++ { go w.worker() } } @@ -41,17 +41,16 @@ func (w *OrderWorker) worker() { return case orderNum, ok := <-w.orderQueue: if !ok { - logger.Log.Info("order queue is closed") + w.logger.Info("order queue is closed") return } result, sleepSeconds, err := client.CheckOrder(orderNum) if err != nil { - logger.Log.Info(err.Error()) + w.logger.Info(err.Error()) } else { - err = w.orderService.ChangeStatus(w.ctx, result) - if err != nil { - logger.Log.Info(err.Error()) + if err := w.orderService.ChangeStatus(w.ctx, result); err != nil { + w.logger.Info(err.Error()) } }