From d412cb8511195afd120596d4f89ee35b7695b356 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sun, 12 Nov 2023 23:47:29 +0200 Subject: [PATCH 01/29] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0.=20=D0=9F=D0=BE?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=84=D0=B8=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 +++ .gitignore | 2 ++ cmd/gophermart/main.go | 11 ++++++- go.mod | 10 +++++++ internal/config/config.go | 50 +++++++++++++++++++++++++++++++ internal/gophermart/gophermart.go | 19 ++++++++++++ 6 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 go.mod create mode 100644 internal/config/config.go create mode 100644 internal/gophermart/gophermart.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6ced4e9 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +SECRET_KEY=yoursecretkey +RUN_ADDRESS=http://localhost:8080 +DATABASE_URI='host=localhost user=admin password=123123 dbname=gophermat sslmode=disable' +ACCRUAL_SYSTEM_ADDRESS=http://localhost:8081 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 50d43ce..ddddf03 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ vendor/ # IDEs directories .idea .vscode + +.env diff --git a/cmd/gophermart/main.go b/cmd/gophermart/main.go index 38dd16d..fbc29b4 100644 --- a/cmd/gophermart/main.go +++ b/cmd/gophermart/main.go @@ -1,3 +1,12 @@ package main -func main() {} +import ( + "github.com/nessai1/gophermat/internal/gophermart" + "log" +) + +func main() { + if err := gophermart.Start(); err != nil { + log.Fatalf("Error while listening application: %w", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2eeba31 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/nessai1/gophermat + +go 1.21.3 + + +require ( + github.com/go-chi/chi v1.5.4 + github.com/stretchr/testify v1.8.4 + go.uber.org/zap v1.24.0 +) \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..f3fbca2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,50 @@ +package config + +import ( + "flag" + "os" + "sync" +) + +type Config struct { + ServiceAddr string + AccrualServiceAddr string + DBConnectionStr string + SecretKey string +} + +var config *Config +var once sync.Once + +func GetConfig() *Config { + once.Do(func() { + config = fetchConfig() + }) + + return config +} + +func fetchConfig() *Config { + serviceAddr := flag.String("a", "", "Address of service") + databaseConnection := flag.String("d", "", "Database connection uri") + accrualServiceAddr := flag.String("r", "", "Accrual service url") + + if serviceAddrEnv := os.Getenv("RUN_ADDRESS"); serviceAddrEnv != "" { + *serviceAddr = serviceAddrEnv + } + + if databaseConnectionEnv := os.Getenv("DATABASE_URI"); databaseConnectionEnv != "" { + *databaseConnection = databaseConnectionEnv + } + + if accrualServiceAddrEnv := os.Getenv("ACCRUAL_SYSTEM_ADDRESS"); accrualServiceAddrEnv != "" { + *accrualServiceAddr = accrualServiceAddrEnv + } + + return &Config{ + ServiceAddr: *serviceAddr, + AccrualServiceAddr: *accrualServiceAddr, + DBConnectionStr: *databaseConnection, + SecretKey: os.Getenv("ACCRUAL_SYSTEM_ADDRESS"), + } +} diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go new file mode 100644 index 0000000..115cee1 --- /dev/null +++ b/internal/gophermart/gophermart.go @@ -0,0 +1,19 @@ +package gophermart + +import ( + "github.com/nessai1/gophermat/internal/config" + "net/http" + + "github.com/go-chi/chi" +) + +func Start() error { + router := chi.NewRouter() + cfg := config.GetConfig() + + if err := http.ListenAndServe(cfg.ServiceAddr, router); err != nil { + return err + } + + return nil +} From fcb602944a7948b4c37517fad8f2ef69933a1760 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 13 Nov 2023 00:02:49 +0200 Subject: [PATCH 02/29] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=D0=B7=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 5 +++-- go.sum | 6 ++++++ internal/config/config.go | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 go.sum diff --git a/go.mod b/go.mod index 2eeba31..13bfc46 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,10 @@ module github.com/nessai1/gophermat go 1.21.3 - require ( github.com/go-chi/chi v1.5.4 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.24.0 -) \ No newline at end of file +) + +require github.com/joho/godotenv v1.5.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a57ea76 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= diff --git a/internal/config/config.go b/internal/config/config.go index f3fbca2..b0541ae 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "flag" + "github.com/joho/godotenv" "os" "sync" ) @@ -25,10 +26,14 @@ func GetConfig() *Config { } func fetchConfig() *Config { + serviceAddr := flag.String("a", "", "Address of service") databaseConnection := flag.String("d", "", "Database connection uri") accrualServiceAddr := flag.String("r", "", "Accrual service url") + flag.Parse() + godotenv.Load() // May not have .env + if serviceAddrEnv := os.Getenv("RUN_ADDRESS"); serviceAddrEnv != "" { *serviceAddr = serviceAddrEnv } From 77074980bf85e2b09b1f09163e06f0015fccb47e Mon Sep 17 00:00:00 2001 From: nessai1 Date: Tue, 14 Nov 2023 21:25:45 +0200 Subject: [PATCH 03/29] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B3=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 3 ++- cmd/gophermart/main.go | 2 +- go.mod | 7 +++-- go.sum | 4 +++ internal/config/config.go | 16 ++++++++++++ internal/gophermart/gophermart.go | 10 +++++++ internal/logger/logger.go | 12 +++++++++ internal/logger/zap-logger.go | 43 +++++++++++++++++++++++++++++++ 8 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 internal/logger/logger.go create mode 100644 internal/logger/zap-logger.go diff --git a/.env.example b/.env.example index 6ced4e9..7185b25 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ SECRET_KEY=yoursecretkey RUN_ADDRESS=http://localhost:8080 DATABASE_URI='host=localhost user=admin password=123123 dbname=gophermat sslmode=disable' -ACCRUAL_SYSTEM_ADDRESS=http://localhost:8081 \ No newline at end of file +ACCRUAL_SYSTEM_ADDRESS=http://localhost:8081 +ENV_TYPE=development \ No newline at end of file diff --git a/cmd/gophermart/main.go b/cmd/gophermart/main.go index fbc29b4..2daf242 100644 --- a/cmd/gophermart/main.go +++ b/cmd/gophermart/main.go @@ -7,6 +7,6 @@ import ( func main() { if err := gophermart.Start(); err != nil { - log.Fatalf("Error while listening application: %w", err) + log.Fatalf("error while listening application: %s", err.Error()) } } diff --git a/go.mod b/go.mod index 13bfc46..9f5514c 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,10 @@ go 1.21.3 require ( github.com/go-chi/chi v1.5.4 github.com/stretchr/testify v1.8.4 - go.uber.org/zap v1.24.0 + go.uber.org/zap v1.26.0 ) -require github.com/joho/godotenv v1.5.1 // indirect +require ( + github.com/joho/godotenv v1.5.1 // indirect + go.uber.org/multierr v1.10.0 // indirect +) diff --git a/go.sum b/go.sum index a57ea76..1583711 100644 --- a/go.sum +++ b/go.sum @@ -3,4 +3,8 @@ github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIu github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= diff --git a/internal/config/config.go b/internal/config/config.go index b0541ae..521291a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,11 +7,18 @@ import ( "sync" ) +type EnvType string + +const EnvTypeDevelopment EnvType = "development" +const EnvTypeStage EnvType = "stage" +const EnvTypeProduction EnvType = "production" + type Config struct { ServiceAddr string AccrualServiceAddr string DBConnectionStr string SecretKey string + EnvType EnvType } var config *Config @@ -46,10 +53,19 @@ func fetchConfig() *Config { *accrualServiceAddr = accrualServiceAddrEnv } + var envType EnvType + envTypeStr := os.Getenv("ENV_TYPE") + if envTypeStr == "" { + envType = EnvTypeProduction + } else { + envType = EnvType(envTypeStr) + } + return &Config{ ServiceAddr: *serviceAddr, AccrualServiceAddr: *accrualServiceAddr, DBConnectionStr: *databaseConnection, SecretKey: os.Getenv("ACCRUAL_SYSTEM_ADDRESS"), + EnvType: envType, } } diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index 115cee1..c542994 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -1,7 +1,10 @@ package gophermart import ( + "fmt" "github.com/nessai1/gophermat/internal/config" + "github.com/nessai1/gophermat/internal/logger" + "go.uber.org/zap" "net/http" "github.com/go-chi/chi" @@ -11,6 +14,13 @@ func Start() error { router := chi.NewRouter() cfg := config.GetConfig() + log, err := logger.NewLogger(cfg.EnvType) + if err != nil { + return fmt.Errorf("cannot initialize logger: %w", err) + } + + log.Info("starting service", zap.String("service address", cfg.ServiceAddr)) + if err := http.ListenAndServe(cfg.ServiceAddr, router); err != nil { return err } diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..95be3d4 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,12 @@ +package logger + +import ( + "github.com/nessai1/gophermat/internal/config" + "go.uber.org/zap" +) + +func NewLogger(envType config.EnvType) (*zap.Logger, error) { + logger, err := createZapLogger(envType) + + return logger, err +} diff --git a/internal/logger/zap-logger.go b/internal/logger/zap-logger.go new file mode 100644 index 0000000..9ec1452 --- /dev/null +++ b/internal/logger/zap-logger.go @@ -0,0 +1,43 @@ +package logger + +import ( + "fmt" + "github.com/nessai1/gophermat/internal/config" + "go.uber.org/zap" + "os" + + "go.uber.org/zap/zapcore" +) + +func createZapLogger(envType config.EnvType) (*zap.Logger, error) { + atom := zap.NewAtomicLevel() + + logLevel, err := getZapLogLevelByEnvLevel(envType) + if err != nil { + return nil, err + } + + atom.SetLevel(logLevel) + encoderCfg := zap.NewProductionEncoderConfig() + encoderCfg.EncodeTime = zapcore.RFC3339TimeEncoder + + logger := zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(encoderCfg), + zapcore.Lock(os.Stdout), + atom, + )) + + return logger, nil +} + +func getZapLogLevelByEnvLevel(envType config.EnvType) (zapcore.Level, error) { + if envType == config.EnvTypeProduction { + return zapcore.ErrorLevel, nil + } else if envType == config.EnvTypeStage { + return zapcore.InfoLevel, nil + } else if envType == config.EnvTypeDevelopment { + return zapcore.DebugLevel, nil + } + + return 0, fmt.Errorf("unexpected EnvType got (%d)", envType) +} From 642f87afd41f98581f1d5bde6bfea57f705b2f13 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Tue, 14 Nov 2023 22:16:52 +0200 Subject: [PATCH 04/29] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83=20=D1=81=20=D0=91=D0=94=20?= =?UTF-8?q?=D0=B2=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 12 ++++++ go.sum | 33 +++++++++++++++ internal/database/database.go | 50 +++++++++++++++++++++++ internal/gophermart/gophermart.go | 10 ++++- migrations/001_create_user_table.down.sql | 1 + migrations/001_create_user_table.up.sql | 6 +++ 6 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 internal/database/database.go create mode 100644 migrations/001_create_user_table.down.sql create mode 100644 migrations/001_create_user_table.up.sql diff --git a/go.mod b/go.mod index 9f5514c..5e62781 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,18 @@ require ( ) require ( + github.com/golang-migrate/migrate/v4 v4.16.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/lib/pq v1.10.9 // indirect + go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/text v0.9.0 // indirect ) diff --git a/go.sum b/go.sum index 1583711..b0515b7 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,43 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= +github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= +github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..bb4205f --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,50 @@ +package database + +import ( + "database/sql" + "errors" + "fmt" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + + _ "github.com/jackc/pgx/v5/stdlib" + + _ "github.com/golang-migrate/migrate/v4/source/file" +) + +func InitSQLDriverByConnectionURI(connectionURI string) (*sql.DB, error) { + db, err := sql.Open("pgx", connectionURI) + if err != nil { + return nil, fmt.Errorf("cannot open sql connection: %w", err) + } + + err = db.Ping() + if err != nil { + return nil, fmt.Errorf("cannot ping database: %w", err) + } + + err = initMigrations(db) + if err != nil && !errors.Is(err, migrate.ErrNoChange) { + return nil, fmt.Errorf("cannot init migrations for sql connection: %w", err) + } + + return db, nil +} + +func initMigrations(db *sql.DB) error { + driver, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + return fmt.Errorf("error while create driver with instance: %w", err) + } + + migrations, err := migrate.NewWithDatabaseInstance("file:migrations", "postgres", driver) + if err != nil { + return fmt.Errorf("error while create migrations: %w", err) + } + + if err = migrations.Up(); err != nil { + return err + } + + return nil +} diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index c542994..793a133 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -3,6 +3,7 @@ package gophermart import ( "fmt" "github.com/nessai1/gophermat/internal/config" + "github.com/nessai1/gophermat/internal/database" "github.com/nessai1/gophermat/internal/logger" "go.uber.org/zap" "net/http" @@ -16,9 +17,16 @@ func Start() error { log, err := logger.NewLogger(cfg.EnvType) if err != nil { - return fmt.Errorf("cannot initialize logger: %w", err) + return fmt.Errorf("cannot initialize logger on start service: %w", err) } + db, err := database.InitSQLDriverByConnectionURI(cfg.DBConnectionStr) + if err != nil { + return fmt.Errorf("cannot initialize database on start service: %w", err) + } + + db.Ping() + log.Info("starting service", zap.String("service address", cfg.ServiceAddr)) if err := http.ListenAndServe(cfg.ServiceAddr, router); err != nil { diff --git a/migrations/001_create_user_table.down.sql b/migrations/001_create_user_table.down.sql new file mode 100644 index 0000000..96b591d --- /dev/null +++ b/migrations/001_create_user_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "user"; \ No newline at end of file diff --git a/migrations/001_create_user_table.up.sql b/migrations/001_create_user_table.up.sql new file mode 100644 index 0000000..a666a04 --- /dev/null +++ b/migrations/001_create_user_table.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS "user" ( + id serial primary key, + login varchar(255) not null unique, + password varchar(255) not null, + balance real not null default 0.0 +) \ No newline at end of file From ba6e84a0f4c28c008dc12c100109dab7c4c02bd9 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Wed, 15 Nov 2023 23:42:25 +0200 Subject: [PATCH 05/29] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8C,=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=20=D1=8E=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=B2,=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=BD=D0=B5=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 4 +++ go.sum | 10 ++++++ internal/user/user.go | 51 ++++++++++++++++++++++++++++++ internal/user/user_test.go | 65 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 internal/user/user.go create mode 100644 internal/user/user_test.go diff --git a/go.mod b/go.mod index 5e62781..040f348 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang-migrate/migrate/v4 v4.16.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -18,9 +19,12 @@ require ( github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/sync v0.2.0 // indirect golang.org/x/text v0.9.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b0515b7..8c1da6e 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= @@ -21,10 +22,17 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -41,3 +49,5 @@ golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/user/user.go b/internal/user/user.go new file mode 100644 index 0000000..23ab4ee --- /dev/null +++ b/internal/user/user.go @@ -0,0 +1,51 @@ +package user + +import ( + "crypto/sha256" + "errors" + "fmt" +) + +var ErrUserNotFound = errors.New("user not found") +var ErrIncorrectUserPassword = errors.New("user password is wrong") + +type User struct { + Login string + Balance float32 + + password string +} + +type Repository interface { + GetUserByLogin(string) (*User, error) +} + +type Controller struct { + repository Repository +} + +func NewController(repository Repository) Controller { + return Controller{repository: repository} +} + +func (controller *Controller) GetUserByCredentials(login, password string) (*User, error) { + user, err := controller.repository.GetUserByLogin(login) + + if err != nil && errors.Is(err, ErrUserNotFound) { + return nil, err + } else if err != nil { + return nil, fmt.Errorf("unhandled error while getting user by credetials: %w", err) + } + + if user.password != buildPasswordHash(password) { + return nil, ErrIncorrectUserPassword + } + + return user, nil +} + +func buildPasswordHash(password string) string { + shaSum := sha256.Sum256([]byte(password)) + + return fmt.Sprintf("%x", shaSum) +} diff --git a/internal/user/user_test.go b/internal/user/user_test.go new file mode 100644 index 0000000..5e0678a --- /dev/null +++ b/internal/user/user_test.go @@ -0,0 +1,65 @@ +package user + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +type TestRepository struct { + data map[string]User +} + +func (repository *TestRepository) GetUserByLogin(login string) (*User, error) { + user, isFind := repository.data[login] + + if !isFind { + return nil, ErrUserNotFound + } + + return &user, nil +} + +func TestController_GetUserByCredentials(t *testing.T) { + userOnePassword := "superSecret" + userOneLogin := "userOne" + userOneBalance := float32(300) + users := map[string]User{ + userOneLogin: { + Login: userOneLogin, + Balance: userOneBalance, + password: buildPasswordHash(userOnePassword), + }, + } + + controller := NewController(&TestRepository{data: users}) + + user, err := controller.GetUserByCredentials(userOneLogin, userOnePassword) + if assert.NoErrorf(t, err, "user with login %s must be found", userOneLogin) { + assert.Equalf(t, userOneLogin, user.Login, "user login not equeal (%s != %s)", userOneLogin, user.Login) + assert.Equalf(t, userOneBalance, user.Balance, "user balance not equeal (%f != %f)", userOneBalance, user.Balance) + } + + user, err = controller.GetUserByCredentials(userOneLogin, "superSecrets") + assert.ErrorIs(t, err, ErrIncorrectUserPassword, "method must be returned incorrect password error") + assert.Nil(t, user, "user pointer must be nil on incorrect password find") + + user, err = controller.GetUserByCredentials("userTwo", userOnePassword) + assert.ErrorIs(t, err, ErrUserNotFound, "method must be returned user not found error") + assert.Nil(t, user, "user pointer must be nil on incorrect login find") +} + +func Test_buildPasswordHash(t *testing.T) { + superSecret := "superSecret" + superSecretHash := "056b7fe47141b6e48e87caf8f8e5bb92120ac12c6e6944cf7dbcda2db23581cc" + + superSecrets := "superSecrets" + + h := buildPasswordHash(superSecret) + assert.Truef(t, h == superSecretHash, "hash must be equal (%s == %s)", h, superSecretHash) + + h = buildPasswordHash(superSecrets) + assert.Falsef(t, h == superSecretHash, "hash must be not equal (%s != %s)", h, superSecretHash) + + h = buildPasswordHash(superSecret) // second check for invariance + assert.Truef(t, h == superSecretHash, "hash must be equal (%s == %s)", h, superSecretHash) +} From a95957fa54551c842c2ad59e7f518a8306eccc0b Mon Sep 17 00:00:00 2001 From: nessai1 Date: Thu, 16 Nov 2023 00:41:53 +0200 Subject: [PATCH 06/29] =?UTF-8?q?=D0=9C=D0=B5=D1=82=D0=BE=D0=B4=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=8E?= =?UTF-8?q?=D0=B7=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/user/user.go | 22 ++++++++++++++++++++ internal/user/user_test.go | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/internal/user/user.go b/internal/user/user.go index 23ab4ee..63700dc 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -8,6 +8,7 @@ import ( var ErrUserNotFound = errors.New("user not found") var ErrIncorrectUserPassword = errors.New("user password is wrong") +var ErrLoginAlreadyExists = errors.New("input user login already exists") type User struct { Login string @@ -18,6 +19,7 @@ type User struct { type Repository interface { GetUserByLogin(string) (*User, error) + CreateUser(user *User) error } type Controller struct { @@ -44,6 +46,26 @@ func (controller *Controller) GetUserByCredentials(login, password string) (*Use return user, nil } +func (controller *Controller) AddUser(login, password string) (*User, error) { + passwordHash := buildPasswordHash(password) + + user := User{ + Login: login, + Balance: 0, + + password: passwordHash, + } + + err := controller.repository.CreateUser(&user) + if err != nil && !errors.Is(err, ErrLoginAlreadyExists) { + return nil, err + } else if err != nil { + return nil, fmt.Errorf("repository error while add user in controller: %w", err) + } + + return &user, nil +} + func buildPasswordHash(password string) string { shaSum := sha256.Sum256([]byte(password)) diff --git a/internal/user/user_test.go b/internal/user/user_test.go index 5e0678a..899ab15 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -2,6 +2,7 @@ package user import ( "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" ) @@ -19,6 +20,16 @@ func (repository *TestRepository) GetUserByLogin(login string) (*User, error) { return &user, nil } +func (repository *TestRepository) CreateUser(user *User) error { + _, isFound := repository.data[user.Login] + if isFound { + return ErrLoginAlreadyExists + } + + repository.data[user.Login] = *user + return nil +} + func TestController_GetUserByCredentials(t *testing.T) { userOnePassword := "superSecret" userOneLogin := "userOne" @@ -48,6 +59,36 @@ func TestController_GetUserByCredentials(t *testing.T) { assert.Nil(t, user, "user pointer must be nil on incorrect login find") } +func TestController_AddUser(t *testing.T) { + repository := TestRepository{data: map[string]User{}} + controller := NewController(&repository) + + userLogin := "userOne" + userPassword := "passwordOne" + + _, err := controller.GetUserByCredentials(userLogin, userPassword) + require.ErrorIs(t, err, ErrUserNotFound) + + user, err := controller.AddUser(userLogin, userPassword) + require.NoError(t, err) + require.Equalf(t, len(repository.data), 1, "repository len must be 1, got %d", len(repository.data)) + + expectedUser := User{ + Login: userLogin, + Balance: 0, + password: buildPasswordHash(userPassword), + } + + assert.Equal(t, expectedUser, *user, "created and expected users are not equal") + + secondUser, err := controller.GetUserByCredentials(userLogin, userPassword) + require.NoError(t, err) + assert.Equal(t, *secondUser, *user) + + _, err = controller.AddUser(userLogin, userPassword) + require.ErrorIs(t, err, ErrLoginAlreadyExists) +} + func Test_buildPasswordHash(t *testing.T) { superSecret := "superSecret" superSecretHash := "056b7fe47141b6e48e87caf8f8e5bb92120ac12c6e6944cf7dbcda2db23581cc" From a1854fea4bc7361d384aaf042d04b7dd7a70f001 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Fri, 17 Nov 2023 00:32:31 +0200 Subject: [PATCH 07/29] =?UTF-8?q?=D0=A0=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D0=B9=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=20postgres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/user/pgx-repository.go | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 internal/user/pgx-repository.go diff --git a/internal/user/pgx-repository.go b/internal/user/pgx-repository.go new file mode 100644 index 0000000..62cbf6c --- /dev/null +++ b/internal/user/pgx-repository.go @@ -0,0 +1,48 @@ +package user + +import ( + "database/sql" + "errors" + "fmt" + "github.com/jackc/pgx/v5/pgconn" +) + +type PGXRepository struct { + db *sql.DB +} + +func (repository *PGXRepository) GetUserByLogin(login string) (*User, error) { + row := repository.db.QueryRow("SELECT login, password, balance FROM user WHERE login = $1", login) + + if row.Err() != nil && errors.Is(row.Err(), sql.ErrNoRows) { + return nil, ErrUserNotFound + } else if row.Err() != nil { + return nil, fmt.Errorf("error while query for get user by login: %w", row.Err()) + } + + var user User + + err := row.Scan(&user.Login, &user.password, &user.Balance) + if err != nil { + return nil, fmt.Errorf("error while scan row for get user by login: %w", err) + } + + return &user, nil +} + +func (repository *PGXRepository) CreateUser(user *User) error { + _, err := repository.db.Exec("INSERT INTO user(login, password, balance) VALUES ($1, $2, $3)", user.Login, user.password, user.Balance) + + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + if pgErr.Code == "23505" { + return ErrLoginAlreadyExists + } + } + + return fmt.Errorf("error while creating user: %w", err) + } + + return nil +} From 34a840560896a044a4f59d3286f6efc73307ceed Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sat, 18 Nov 2023 12:59:50 +0200 Subject: [PATCH 08/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82=20?= =?UTF-8?q?=D0=BA=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D1=8F=D0=BC?= =?UTF-8?q?=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F/=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8F;=20=D0=9A=D0=BE=D0=BD=D0=BA?= =?UTF-8?q?=D1=80=D0=B5=D1=82=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=BA=D0=BE=D0=B4=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=83=D0=BD=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=BA=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D1=83=20=D0=91=D0=94=20=D0=BD=D0=B0=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=8E=D0=B7=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/user/pgx-repository.go | 13 ++++++++----- internal/user/user.go | 13 +++++++------ internal/user/user_test.go | 22 +++++++++++++--------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/internal/user/pgx-repository.go b/internal/user/pgx-repository.go index 62cbf6c..a181508 100644 --- a/internal/user/pgx-repository.go +++ b/internal/user/pgx-repository.go @@ -1,6 +1,7 @@ package user import ( + "context" "database/sql" "errors" "fmt" @@ -11,8 +12,10 @@ type PGXRepository struct { db *sql.DB } -func (repository *PGXRepository) GetUserByLogin(login string) (*User, error) { - row := repository.db.QueryRow("SELECT login, password, balance FROM user WHERE login = $1", login) +const PostgresErrCodeUniqueViolation = "23505" + +func (repository *PGXRepository) GetUserByLogin(ctx context.Context, login string) (*User, error) { + row := repository.db.QueryRowContext(ctx, "SELECT login, password, balance FROM user WHERE login = $1", login) if row.Err() != nil && errors.Is(row.Err(), sql.ErrNoRows) { return nil, ErrUserNotFound @@ -30,13 +33,13 @@ func (repository *PGXRepository) GetUserByLogin(login string) (*User, error) { return &user, nil } -func (repository *PGXRepository) CreateUser(user *User) error { - _, err := repository.db.Exec("INSERT INTO user(login, password, balance) VALUES ($1, $2, $3)", user.Login, user.password, user.Balance) +func (repository *PGXRepository) CreateUser(ctx context.Context, user *User) error { + _, err := repository.db.ExecContext(ctx, "INSERT INTO user(login, password, balance) VALUES ($1, $2, $3)", user.Login, user.password, user.Balance) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { - if pgErr.Code == "23505" { + if pgErr.Code == PostgresErrCodeUniqueViolation { return ErrLoginAlreadyExists } } diff --git a/internal/user/user.go b/internal/user/user.go index 63700dc..86fdd64 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -1,6 +1,7 @@ package user import ( + "context" "crypto/sha256" "errors" "fmt" @@ -18,8 +19,8 @@ type User struct { } type Repository interface { - GetUserByLogin(string) (*User, error) - CreateUser(user *User) error + GetUserByLogin(context.Context, string) (*User, error) + CreateUser(context.Context, *User) error } type Controller struct { @@ -30,8 +31,8 @@ func NewController(repository Repository) Controller { return Controller{repository: repository} } -func (controller *Controller) GetUserByCredentials(login, password string) (*User, error) { - user, err := controller.repository.GetUserByLogin(login) +func (controller *Controller) GetUserByCredentials(ctx context.Context, login, password string) (*User, error) { + user, err := controller.repository.GetUserByLogin(ctx, login) if err != nil && errors.Is(err, ErrUserNotFound) { return nil, err @@ -46,7 +47,7 @@ func (controller *Controller) GetUserByCredentials(login, password string) (*Use return user, nil } -func (controller *Controller) AddUser(login, password string) (*User, error) { +func (controller *Controller) AddUser(ctx context.Context, login, password string) (*User, error) { passwordHash := buildPasswordHash(password) user := User{ @@ -56,7 +57,7 @@ func (controller *Controller) AddUser(login, password string) (*User, error) { password: passwordHash, } - err := controller.repository.CreateUser(&user) + err := controller.repository.CreateUser(ctx, &user) if err != nil && !errors.Is(err, ErrLoginAlreadyExists) { return nil, err } else if err != nil { diff --git a/internal/user/user_test.go b/internal/user/user_test.go index 899ab15..3541dca 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -1,6 +1,7 @@ package user import ( + "context" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -10,7 +11,7 @@ type TestRepository struct { data map[string]User } -func (repository *TestRepository) GetUserByLogin(login string) (*User, error) { +func (repository *TestRepository) GetUserByLogin(_ context.Context, login string) (*User, error) { user, isFind := repository.data[login] if !isFind { @@ -20,7 +21,7 @@ func (repository *TestRepository) GetUserByLogin(login string) (*User, error) { return &user, nil } -func (repository *TestRepository) CreateUser(user *User) error { +func (repository *TestRepository) CreateUser(_ context.Context, user *User) error { _, isFound := repository.data[user.Login] if isFound { return ErrLoginAlreadyExists @@ -43,18 +44,19 @@ func TestController_GetUserByCredentials(t *testing.T) { } controller := NewController(&TestRepository{data: users}) + ctx := context.TODO() - user, err := controller.GetUserByCredentials(userOneLogin, userOnePassword) + user, err := controller.GetUserByCredentials(ctx, userOneLogin, userOnePassword) if assert.NoErrorf(t, err, "user with login %s must be found", userOneLogin) { assert.Equalf(t, userOneLogin, user.Login, "user login not equeal (%s != %s)", userOneLogin, user.Login) assert.Equalf(t, userOneBalance, user.Balance, "user balance not equeal (%f != %f)", userOneBalance, user.Balance) } - user, err = controller.GetUserByCredentials(userOneLogin, "superSecrets") + user, err = controller.GetUserByCredentials(ctx, userOneLogin, "superSecrets") assert.ErrorIs(t, err, ErrIncorrectUserPassword, "method must be returned incorrect password error") assert.Nil(t, user, "user pointer must be nil on incorrect password find") - user, err = controller.GetUserByCredentials("userTwo", userOnePassword) + user, err = controller.GetUserByCredentials(ctx, "userTwo", userOnePassword) assert.ErrorIs(t, err, ErrUserNotFound, "method must be returned user not found error") assert.Nil(t, user, "user pointer must be nil on incorrect login find") } @@ -66,10 +68,12 @@ func TestController_AddUser(t *testing.T) { userLogin := "userOne" userPassword := "passwordOne" - _, err := controller.GetUserByCredentials(userLogin, userPassword) + ctx := context.TODO() + + _, err := controller.GetUserByCredentials(ctx, userLogin, userPassword) require.ErrorIs(t, err, ErrUserNotFound) - user, err := controller.AddUser(userLogin, userPassword) + user, err := controller.AddUser(ctx, userLogin, userPassword) require.NoError(t, err) require.Equalf(t, len(repository.data), 1, "repository len must be 1, got %d", len(repository.data)) @@ -81,11 +85,11 @@ func TestController_AddUser(t *testing.T) { assert.Equal(t, expectedUser, *user, "created and expected users are not equal") - secondUser, err := controller.GetUserByCredentials(userLogin, userPassword) + secondUser, err := controller.GetUserByCredentials(ctx, userLogin, userPassword) require.NoError(t, err) assert.Equal(t, *secondUser, *user) - _, err = controller.AddUser(userLogin, userPassword) + _, err = controller.AddUser(ctx, userLogin, userPassword) require.ErrorIs(t, err, ErrLoginAlreadyExists) } From 135c09e687ceea92547e309e1b3c9ebdbf2a2ac4 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sat, 18 Nov 2023 14:49:50 +0200 Subject: [PATCH 09/29] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8/=D1=80=D0=B5=D0=B3=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8.=20=D0=9D=D0=B0?= =?UTF-8?q?=D1=87=D0=B0=D0=BB=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/config.go | 3 ++- internal/database/database.go | 1 + internal/gophermart/gophermart.go | 14 ++++++++++++-- internal/handler/auth-handler.go | 31 +++++++++++++++++++++++++++++++ internal/logger/logger.go | 1 + internal/logger/zap-logger.go | 2 +- internal/user/pgx-repository.go | 5 +++++ internal/user/user.go | 4 ++-- internal/user/user_test.go | 3 ++- 9 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 internal/handler/auth-handler.go diff --git a/internal/config/config.go b/internal/config/config.go index 521291a..f7012e4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,9 +2,10 @@ package config import ( "flag" - "github.com/joho/godotenv" "os" "sync" + + "github.com/joho/godotenv" ) type EnvType string diff --git a/internal/database/database.go b/internal/database/database.go index bb4205f..1e53d1d 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -4,6 +4,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/postgres" diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index 793a133..2c01eea 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -4,11 +4,13 @@ import ( "fmt" "github.com/nessai1/gophermat/internal/config" "github.com/nessai1/gophermat/internal/database" + "github.com/nessai1/gophermat/internal/handler" "github.com/nessai1/gophermat/internal/logger" - "go.uber.org/zap" + "github.com/nessai1/gophermat/internal/user" "net/http" "github.com/go-chi/chi" + "go.uber.org/zap" ) func Start() error { @@ -25,7 +27,15 @@ func Start() error { return fmt.Errorf("cannot initialize database on start service: %w", err) } - db.Ping() + authHandler := handler.AuthHandler{ + Logger: log, + UserController: user.NewController(user.CreatePGXRepository(db)), + } + + router.Use(authHandler.MiddlewareAuthorizeRequest()) + + router.HandleFunc("/api/user/register", authHandler.HandleRegisterUser) + router.HandleFunc("/api/user/login", authHandler.HandleAuthUser) log.Info("starting service", zap.String("service address", cfg.ServiceAddr)) diff --git a/internal/handler/auth-handler.go b/internal/handler/auth-handler.go new file mode 100644 index 0000000..6f560c0 --- /dev/null +++ b/internal/handler/auth-handler.go @@ -0,0 +1,31 @@ +package handler + +import ( + "github.com/nessai1/gophermat/internal/user" + "net/http" + + "go.uber.org/zap" +) + +type AuthHandler struct { + Logger *zap.Logger + SecretKey string + UserController *user.Controller +} + +func (handler *AuthHandler) HandleAuthUser(writer http.ResponseWriter, request *http.Request) { + writer.Write([]byte("auth")) +} + +func (handler *AuthHandler) HandleRegisterUser(writer http.ResponseWriter, request *http.Request) { + writer.Write([]byte("register")) +} + +func (handler *AuthHandler) MiddlewareAuthorizeRequest() func(handler http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + handler.Logger.Info("Got middleware") + next.ServeHTTP(writer, request) + }) + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 95be3d4..69992d0 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -2,6 +2,7 @@ package logger import ( "github.com/nessai1/gophermat/internal/config" + "go.uber.org/zap" ) diff --git a/internal/logger/zap-logger.go b/internal/logger/zap-logger.go index 9ec1452..d8e2763 100644 --- a/internal/logger/zap-logger.go +++ b/internal/logger/zap-logger.go @@ -3,9 +3,9 @@ package logger import ( "fmt" "github.com/nessai1/gophermat/internal/config" - "go.uber.org/zap" "os" + "go.uber.org/zap" "go.uber.org/zap/zapcore" ) diff --git a/internal/user/pgx-repository.go b/internal/user/pgx-repository.go index a181508..c2ee025 100644 --- a/internal/user/pgx-repository.go +++ b/internal/user/pgx-repository.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/jackc/pgx/v5/pgconn" ) @@ -14,6 +15,10 @@ type PGXRepository struct { const PostgresErrCodeUniqueViolation = "23505" +func CreatePGXRepository(db *sql.DB) *PGXRepository { + return &PGXRepository{db: db} +} + func (repository *PGXRepository) GetUserByLogin(ctx context.Context, login string) (*User, error) { row := repository.db.QueryRowContext(ctx, "SELECT login, password, balance FROM user WHERE login = $1", login) diff --git a/internal/user/user.go b/internal/user/user.go index 86fdd64..34797d1 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -27,8 +27,8 @@ type Controller struct { repository Repository } -func NewController(repository Repository) Controller { - return Controller{repository: repository} +func NewController(repository Repository) *Controller { + return &Controller{repository: repository} } func (controller *Controller) GetUserByCredentials(ctx context.Context, login, password string) (*User, error) { diff --git a/internal/user/user_test.go b/internal/user/user_test.go index 3541dca..37efd24 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -2,9 +2,10 @@ package user import ( "context" + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) type TestRepository struct { From 05d6b12f718c72f6f1562ac047492b155f213f45 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sun, 19 Nov 2023 13:53:32 +0200 Subject: [PATCH 10/29] =?UTF-8?q?=D0=92=D1=8B=D0=BD=D0=B5=D1=81=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B9=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=8E=D1=89=D0=B8=D0=B9=20?= =?UTF-8?q?=D1=81=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D0=B9=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B8=D1=81=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B4=D1=80=D1=83=D0=B3=D0=B8=D1=85=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/user/map-repository.go | 27 +++++++++++++++++++++++++++ internal/user/user.go | 5 +++++ internal/user/user_test.go | 28 ++-------------------------- 3 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 internal/user/map-repository.go diff --git a/internal/user/map-repository.go b/internal/user/map-repository.go new file mode 100644 index 0000000..d77bcd6 --- /dev/null +++ b/internal/user/map-repository.go @@ -0,0 +1,27 @@ +package user + +import "context" + +type MapRepository struct { + data map[string]User +} + +func (repository *MapRepository) GetUserByLogin(_ context.Context, login string) (*User, error) { + user, isFind := repository.data[login] + + if !isFind { + return nil, ErrUserNotFound + } + + return &user, nil +} + +func (repository *MapRepository) CreateUser(_ context.Context, user *User) error { + _, isFound := repository.data[user.Login] + if isFound { + return ErrLoginAlreadyExists + } + + repository.data[user.Login] = *user + return nil +} diff --git a/internal/user/user.go b/internal/user/user.go index 34797d1..022905e 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -47,6 +47,11 @@ func (controller *Controller) GetUserByCredentials(ctx context.Context, login, p return user, nil } +func (controller *Controller) GetUserByLogin(ctx context.Context, login string) (*User, error) { + user, err := controller.repository.GetUserByLogin(ctx, login) + return user, err +} + func (controller *Controller) AddUser(ctx context.Context, login, password string) (*User, error) { passwordHash := buildPasswordHash(password) diff --git a/internal/user/user_test.go b/internal/user/user_test.go index 37efd24..1f15c1c 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -8,30 +8,6 @@ import ( "github.com/stretchr/testify/require" ) -type TestRepository struct { - data map[string]User -} - -func (repository *TestRepository) GetUserByLogin(_ context.Context, login string) (*User, error) { - user, isFind := repository.data[login] - - if !isFind { - return nil, ErrUserNotFound - } - - return &user, nil -} - -func (repository *TestRepository) CreateUser(_ context.Context, user *User) error { - _, isFound := repository.data[user.Login] - if isFound { - return ErrLoginAlreadyExists - } - - repository.data[user.Login] = *user - return nil -} - func TestController_GetUserByCredentials(t *testing.T) { userOnePassword := "superSecret" userOneLogin := "userOne" @@ -44,7 +20,7 @@ func TestController_GetUserByCredentials(t *testing.T) { }, } - controller := NewController(&TestRepository{data: users}) + controller := NewController(&MapRepository{data: users}) ctx := context.TODO() user, err := controller.GetUserByCredentials(ctx, userOneLogin, userOnePassword) @@ -63,7 +39,7 @@ func TestController_GetUserByCredentials(t *testing.T) { } func TestController_AddUser(t *testing.T) { - repository := TestRepository{data: map[string]User{}} + repository := MapRepository{data: map[string]User{}} controller := NewController(&repository) userLogin := "userOne" From d21935ff525cac2d24a3be45a7adb69bd870cd16 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 20 Nov 2023 00:59:22 +0200 Subject: [PATCH 11/29] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8/=D1=80=D0=B5=D0=B3=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 + go.sum | 2 + internal/gophermart/gophermart.go | 14 ++- internal/handler/auth-handler.go | 191 +++++++++++++++++++++++++++++- internal/handler/order-handler.go | 10 ++ internal/user/pgx-repository.go | 12 +- 6 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 internal/handler/order-handler.go diff --git a/go.mod b/go.mod index 040f348..669d1e4 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-migrate/migrate/v4 v4.16.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/go.sum b/go.sum index 8c1da6e..31f281e 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index 2c01eea..49b5042 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -32,10 +32,18 @@ func Start() error { UserController: user.NewController(user.CreatePGXRepository(db)), } - router.Use(authHandler.MiddlewareAuthorizeRequest()) + authMux := chi.NewMux() + authMux.HandleFunc("/api/user/register", authHandler.HandleRegisterUser) + authMux.HandleFunc("/api/user/login", authHandler.HandleAuthUser) - router.HandleFunc("/api/user/register", authHandler.HandleRegisterUser) - router.HandleFunc("/api/user/login", authHandler.HandleAuthUser) + orderHandler := handler.OrderHandler{} + + orderMux := chi.NewMux() + orderMux.Use(authHandler.MiddlewareAuthorizeRequest()) + orderMux.HandleFunc("/", orderHandler.HandleGetUserOrders) + + router.Mount("/", authMux) + router.Mount("/api/user/orders", orderMux) log.Info("starting service", zap.String("service address", cfg.ServiceAddr)) diff --git a/internal/handler/auth-handler.go b/internal/handler/auth-handler.go index 6f560c0..de19173 100644 --- a/internal/handler/auth-handler.go +++ b/internal/handler/auth-handler.go @@ -1,12 +1,37 @@ package handler import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" "github.com/nessai1/gophermat/internal/user" "net/http" + "time" + "github.com/golang-jwt/jwt/v4" "go.uber.org/zap" ) +const authCookieName = "GOPHERMAT_JWT" +const AuthorizeUserContext AuthorizeUserContextKey = "AuthorizeUserContext" +const TokenTTL = time.Hour * 24 + +type AuthorizeUserContextKey string + +var ErrWrongSign = errors.New("got wrong sign") + +type UserCredentials struct { + Login string `json:"login"` + Password string `json:"password"` +} + +type userJWTClaims struct { + jwt.RegisteredClaims + Login string +} + type AuthHandler struct { Logger *zap.Logger SecretKey string @@ -14,18 +39,178 @@ type AuthHandler struct { } func (handler *AuthHandler) HandleAuthUser(writer http.ResponseWriter, request *http.Request) { - writer.Write([]byte("auth")) + + var buffer bytes.Buffer + _, err := buffer.ReadFrom(request.Body) + if err != nil { + handler.Logger.Debug("client sends invalid request", zap.Error(err)) + writer.WriteHeader(http.StatusBadRequest) + return + } + + var credentials UserCredentials + err = json.Unmarshal(buffer.Bytes(), &credentials) + if err != nil { + handler.Logger.Debug("cannot unmarshal client request", zap.Error(err)) + writer.WriteHeader(http.StatusBadRequest) + return + } + + if credentials.Password == "" || credentials.Login == "" { + handler.Logger.Debug("user sends empty login/password") + writer.WriteHeader(http.StatusBadRequest) + return + } + + fetchedUser, err := handler.UserController.GetUserByCredentials(request.Context(), credentials.Login, credentials.Password) + if err != nil && (errors.Is(err, user.ErrUserNotFound) || errors.Is(err, user.ErrIncorrectUserPassword)) { + handler.Logger.Debug("user send invalid credentials on login") + writer.WriteHeader(http.StatusUnauthorized) + return + } else if err != nil { + handler.Logger.Error("error while get user on login", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + sign, err := handler.createSign(fetchedUser) + if err != nil { + handler.Logger.Error("error while sign created account", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + c := &http.Cookie{Name: authCookieName, Value: sign} + http.SetCookie(writer, c) + + ctx := request.Context() + request.WithContext(context.WithValue(ctx, AuthorizeUserContext, fetchedUser)) + + writer.WriteHeader(http.StatusOK) } func (handler *AuthHandler) HandleRegisterUser(writer http.ResponseWriter, request *http.Request) { - writer.Write([]byte("register")) + + var buffer bytes.Buffer + _, err := buffer.ReadFrom(request.Body) + if err != nil { + handler.Logger.Debug("client sends invalid request", zap.Error(err)) + writer.WriteHeader(http.StatusBadRequest) + return + } + + var credentials UserCredentials + err = json.Unmarshal(buffer.Bytes(), &credentials) + if err != nil { + handler.Logger.Debug("cannot unmarshal client request", zap.Error(err)) + writer.WriteHeader(http.StatusBadRequest) + return + } + + if credentials.Password == "" || credentials.Login == "" { + handler.Logger.Debug("user sends empty login/password") + writer.WriteHeader(http.StatusBadRequest) + return + } + + createdUser, err := handler.UserController.AddUser(request.Context(), credentials.Login, credentials.Password) + if err != nil && errors.Is(err, user.ErrLoginAlreadyExists) { + handler.Logger.Debug("user try register existing account", zap.String("login", credentials.Login)) + writer.WriteHeader(http.StatusConflict) + return + } else if err != nil { + handler.Logger.Error("error while create new user by register", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + sign, err := handler.createSign(createdUser) + if err != nil { + handler.Logger.Error("error while sign created account", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + c := &http.Cookie{Name: authCookieName, Value: sign} + http.SetCookie(writer, c) + + ctx := request.Context() + request.WithContext(context.WithValue(ctx, AuthorizeUserContext, createdUser)) + + writer.WriteHeader(http.StatusOK) } func (handler *AuthHandler) MiddlewareAuthorizeRequest() func(handler http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - handler.Logger.Info("Got middleware") + cookie, err := request.Cookie(authCookieName) + if err != nil && errors.Is(err, http.ErrNoCookie) { + handler.Logger.Debug("user has no auth cookie", zap.String("client address", request.RemoteAddr)) + writer.WriteHeader(http.StatusUnauthorized) + return + } else if err != nil { + handler.Logger.Error("undefined error occurred in auth middleware", zap.String("client address", request.RemoteAddr), zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + ctx := request.Context() + authUser, err := handler.fetchUser(ctx, cookie.Value) + if err != nil && errors.Is(err, ErrWrongSign) { + handler.Logger.Debug("user sends invalid sign cookie", zap.Error(err)) + c := &http.Cookie{ + Value: "", + Name: authCookieName, + MaxAge: -1, + } + http.SetCookie(writer, c) + writer.WriteHeader(http.StatusUnauthorized) + return + } else if err != nil { + handler.Logger.Error("error while getting user for auth middleware", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + request.WithContext(context.WithValue(ctx, AuthorizeUserContext, authUser)) + next.ServeHTTP(writer, request) }) } } + +func (handler *AuthHandler) fetchUser(ctx context.Context, sign string) (*user.User, error) { + claims := &userJWTClaims{} + _, err := jwt.ParseWithClaims(sign, claims, func(token *jwt.Token) (interface{}, error) { + return []byte(handler.SecretKey), nil + }) + + if err != nil { + return nil, errors.Join(ErrWrongSign, err) + } + + fetchedUser, err := handler.UserController.GetUserByLogin(ctx, claims.Login) + if err != nil && errors.Is(err, user.ErrUserNotFound) { + return nil, errors.Join(ErrWrongSign, err) + } else if err != nil { + return nil, fmt.Errorf("error while fetch user in jwt: %w", err) + } + + return fetchedUser, nil +} + +func (handler *AuthHandler) createSign(signedUser *user.User) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, userJWTClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenTTL)), + }, + Login: signedUser.Login, + }) + + tokenString, err := token.SignedString([]byte(handler.SecretKey)) + if err != nil { + return "", err + } + + return tokenString, nil +} diff --git a/internal/handler/order-handler.go b/internal/handler/order-handler.go new file mode 100644 index 0000000..bf785cd --- /dev/null +++ b/internal/handler/order-handler.go @@ -0,0 +1,10 @@ +package handler + +import "net/http" + +type OrderHandler struct { +} + +func (handler *OrderHandler) HandleGetUserOrders(writer http.ResponseWriter, request *http.Request) { + +} diff --git a/internal/user/pgx-repository.go b/internal/user/pgx-repository.go index c2ee025..de4d0ec 100644 --- a/internal/user/pgx-repository.go +++ b/internal/user/pgx-repository.go @@ -20,18 +20,18 @@ func CreatePGXRepository(db *sql.DB) *PGXRepository { } func (repository *PGXRepository) GetUserByLogin(ctx context.Context, login string) (*User, error) { - row := repository.db.QueryRowContext(ctx, "SELECT login, password, balance FROM user WHERE login = $1", login) + row := repository.db.QueryRowContext(ctx, "SELECT login, password, balance FROM \"user\" WHERE login = $1", login) - if row.Err() != nil && errors.Is(row.Err(), sql.ErrNoRows) { - return nil, ErrUserNotFound - } else if row.Err() != nil { + if row.Err() != nil { return nil, fmt.Errorf("error while query for get user by login: %w", row.Err()) } var user User err := row.Scan(&user.Login, &user.password, &user.Balance) - if err != nil { + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, ErrUserNotFound + } else if err != nil { return nil, fmt.Errorf("error while scan row for get user by login: %w", err) } @@ -39,7 +39,7 @@ func (repository *PGXRepository) GetUserByLogin(ctx context.Context, login strin } func (repository *PGXRepository) CreateUser(ctx context.Context, user *User) error { - _, err := repository.db.ExecContext(ctx, "INSERT INTO user(login, password, balance) VALUES ($1, $2, $3)", user.Login, user.password, user.Balance) + _, err := repository.db.ExecContext(ctx, "INSERT INTO \"user\" (login, password, balance) VALUES ($1, $2, $3)", user.Login, user.password, user.Balance) if err != nil { var pgErr *pgconn.PgError From 76fb86ad4b7a0362f87023e8af8eeb01386ad1e6 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 20 Nov 2023 01:59:17 +0200 Subject: [PATCH 12/29] =?UTF-8?q?=D0=A1=D1=85=D0=B5=D0=BC=D0=B0=20=D0=91?= =?UTF-8?q?=D0=94=20=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=BC=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=B1=D0=BB=D0=B8=D0=B6=D0=B5=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev/sql_scheme.drawio | 1 + 1 file changed, 1 insertion(+) create mode 100644 dev/sql_scheme.drawio diff --git a/dev/sql_scheme.drawio b/dev/sql_scheme.drawio new file mode 100644 index 0000000..6ab1890 --- /dev/null +++ b/dev/sql_scheme.drawio @@ -0,0 +1 @@ + \ No newline at end of file From 05dd8788652a40eb2a85107db0ab2034f8f75be3 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 20 Nov 2023 22:39:58 +0200 Subject: [PATCH 13/29] =?UTF-8?q?=D0=90=D0=BB=D0=B3=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=82=D0=BC=20=D0=9B=D1=83=D0=BD=D0=B0=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/order/order.go | 30 +++++++++++++++ internal/order/order_test.go | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 internal/order/order.go create mode 100644 internal/order/order_test.go diff --git a/internal/order/order.go b/internal/order/order.go new file mode 100644 index 0000000..dde9674 --- /dev/null +++ b/internal/order/order.go @@ -0,0 +1,30 @@ +package order + +import "strconv" + +func IsOrderNumberCorrect(orderNumber string) bool { + sum := 0 + + numSize := len(orderNumber) + for i := 0; i < numSize; i++ { + num, err := strconv.Atoi(string(orderNumber[numSize-i-1])) + if err != nil { + return false + } + + if i%2 == 1 { + num *= 2 + + if num >= 10 { + buf := strconv.Itoa(num) + v1, _ := strconv.Atoi(string(buf[0])) + v2, _ := strconv.Atoi(string(buf[1])) + num = v1 + v2 + } + } + + sum += num + } + + return sum%10 == 0 +} diff --git a/internal/order/order_test.go b/internal/order/order_test.go new file mode 100644 index 0000000..5c2f019 --- /dev/null +++ b/internal/order/order_test.go @@ -0,0 +1,71 @@ +package order + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestIsOrderNumberCorrect(t *testing.T) { + tests := []struct { + name string + val string + isValid bool + }{ + { + name: "Random string", + val: "iLoveGo", + isValid: false, + }, + { + name: "String with number prefix", + val: "1337Abakada", + isValid: false, + }, + { + name: "Random string #2", + val: "qwertyuiopasdfgh", + isValid: false, + }, + { + name: "Invalid number", + val: "1233444455556666", + isValid: false, + }, + { + name: "Valid number #1", + val: "12345678903", + isValid: true, + }, + { + name: "Valid number #2", + val: "9278923470", + isValid: true, + }, + { + name: "Valid number #3", + val: "346436439", + isValid: true, + }, + { + name: "Valid number #4", + val: "2377225624", + isValid: true, + }, + { + name: "Invalid number #2", + val: "2377225626", + isValid: false, + }, + { + name: "Valid number #3", + val: "9278923473", + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.isValid, IsOrderNumberCorrect(tt.val)) + }) + } +} From a2f93b597872b4134a7aca5f33889f3320320442 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Tue, 21 Nov 2023 00:02:47 +0200 Subject: [PATCH 14/29] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=81=D1=85=D0=B5=D0=BC=D1=83=20=D0=91=D0=94.?= =?UTF-8?q?=20=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BB=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=BE=D0=B2=20=D1=82=D0=B8=D0=BF?= =?UTF-8?q?=D0=B0=20enrollment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev/sql_scheme.drawio | 2 +- migrations/002_create_enrollment_order_table.down.sql | 4 ++++ migrations/002_create_enrollment_order_table.up.sql | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 migrations/002_create_enrollment_order_table.down.sql create mode 100644 migrations/002_create_enrollment_order_table.up.sql diff --git a/dev/sql_scheme.drawio b/dev/sql_scheme.drawio index 6ab1890..59037b1 100644 --- a/dev/sql_scheme.drawio +++ b/dev/sql_scheme.drawio @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/migrations/002_create_enrollment_order_table.down.sql b/migrations/002_create_enrollment_order_table.down.sql new file mode 100644 index 0000000..ae28b0f --- /dev/null +++ b/migrations/002_create_enrollment_order_table.down.sql @@ -0,0 +1,4 @@ +BEGIN; +DROP INDEX IF EXISTS enrollment_order_user_id_idx; +DROP TABLE IF EXISTS enrollment_order; +COMMIT; \ No newline at end of file diff --git a/migrations/002_create_enrollment_order_table.up.sql b/migrations/002_create_enrollment_order_table.up.sql new file mode 100644 index 0000000..282f7da --- /dev/null +++ b/migrations/002_create_enrollment_order_table.up.sql @@ -0,0 +1,10 @@ +BEGIN; +CREATE TABLE enrollment_order ( + order_id char(16) not null PRIMARY KEY, + user_id int not null references "user" (id), + status varchar(30) not null, + accrual real not null default 0.0, + is_accrual_transferred bool not null default false +); +CREATE INDEX enrollment_order_user_id_idx ON enrollment_order (user_id); +COMMIT; \ No newline at end of file From ada29879c5d8f5654c82d245e5870dea07cd0b81 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Tue, 21 Nov 2023 02:06:40 +0200 Subject: [PATCH 15/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20enr?= =?UTF-8?q?ollment.=20=D0=9F=D0=B5=D1=80=D0=B2=D0=B0=D1=8F=20=D1=87=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20?= =?UTF-8?q?=D1=81=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82=D1=8C=D1=8E?= =?UTF-8?q?;=20=D0=9E=D0=BF=D0=B8=D1=81=D0=B0=D0=BB=20=D0=B2=D1=81=D0=B5?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0,=20=D0=BA?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=D0=B5=20/api/user/withdrawals=20(=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=20=D0=B2=D1=81=D0=B5=20=D0=BE=D0=BD=D0=B8=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gophermart/gophermart.go | 28 ++++-- internal/handler/balance-handler.go | 18 ++++ internal/handler/enrollment-order-handler.go | 20 +++++ internal/order/enrollment.go | 91 ++++++++++++++++++++ internal/order/order.go | 7 +- internal/order/pgx-enrollment-repository.go | 29 +++++++ 6 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 internal/handler/balance-handler.go create mode 100644 internal/handler/enrollment-order-handler.go create mode 100644 internal/order/enrollment.go create mode 100644 internal/order/pgx-enrollment-repository.go diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index 49b5042..c247f5c 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -6,6 +6,7 @@ import ( "github.com/nessai1/gophermat/internal/database" "github.com/nessai1/gophermat/internal/handler" "github.com/nessai1/gophermat/internal/logger" + "github.com/nessai1/gophermat/internal/order" "github.com/nessai1/gophermat/internal/user" "net/http" @@ -33,17 +34,30 @@ func Start() error { } authMux := chi.NewMux() - authMux.HandleFunc("/api/user/register", authHandler.HandleRegisterUser) - authMux.HandleFunc("/api/user/login", authHandler.HandleAuthUser) + authMux.Post("/api/user/register", authHandler.HandleRegisterUser) + authMux.Post("/api/user/login", authHandler.HandleAuthUser) - orderHandler := handler.OrderHandler{} + enrollmentController := handler.EnrollmentOrderHandler{ + Logger: log, + EnrollmentController: order.NewEnrollmentController(cfg.AccrualServiceAddr, order.CreatePGXEnrollmentRepository(db)), + } + enrollmentMux := chi.NewMux() + enrollmentMux.Use(authHandler.MiddlewareAuthorizeRequest()) + enrollmentMux.Post("/", enrollmentController.HandleLoadOrders) + enrollmentMux.Get("/", enrollmentController.HandGetOrders) - orderMux := chi.NewMux() - orderMux.Use(authHandler.MiddlewareAuthorizeRequest()) - orderMux.HandleFunc("/", orderHandler.HandleGetUserOrders) + balanceController := handler.BalanceHandler{ + Logger: log, + } + balanceMux := chi.NewMux() + balanceMux.Use(authHandler.MiddlewareAuthorizeRequest()) + balanceMux.Get("/", balanceController.HandleGetBalance) + balanceMux.Post("/withdraw", balanceController.HandleWithdraw) router.Mount("/", authMux) - router.Mount("/api/user/orders", orderMux) + router.Mount("/api/user/orders", enrollmentMux) + router.Mount("/api/user/balance", balanceMux) + // TODO: add /api/user/withdrawals handle log.Info("starting service", zap.String("service address", cfg.ServiceAddr)) diff --git a/internal/handler/balance-handler.go b/internal/handler/balance-handler.go new file mode 100644 index 0000000..0058454 --- /dev/null +++ b/internal/handler/balance-handler.go @@ -0,0 +1,18 @@ +package handler + +import ( + "go.uber.org/zap" + "net/http" +) + +type BalanceHandler struct { + Logger *zap.Logger +} + +func (handler *BalanceHandler) HandleGetBalance(writer http.ResponseWriter, request *http.Request) { + writer.Write([]byte("Getting balance....")) +} + +func (handler *BalanceHandler) HandleWithdraw(writer http.ResponseWriter, request *http.Request) { + writer.Write([]byte("Withdraw....")) +} diff --git a/internal/handler/enrollment-order-handler.go b/internal/handler/enrollment-order-handler.go new file mode 100644 index 0000000..2fb9699 --- /dev/null +++ b/internal/handler/enrollment-order-handler.go @@ -0,0 +1,20 @@ +package handler + +import ( + "github.com/nessai1/gophermat/internal/order" + "go.uber.org/zap" + "net/http" +) + +type EnrollmentOrderHandler struct { + Logger *zap.Logger + EnrollmentController *order.EnrollmentController +} + +func (handler *EnrollmentOrderHandler) HandleLoadOrders(writer http.ResponseWriter, request *http.Request) { + +} + +func (handler *EnrollmentOrderHandler) HandGetOrders(writer http.ResponseWriter, request *http.Request) { + writer.Write([]byte("Getting orders....")) +} diff --git a/internal/order/enrollment.go b/internal/order/enrollment.go new file mode 100644 index 0000000..f94baae --- /dev/null +++ b/internal/order/enrollment.go @@ -0,0 +1,91 @@ +package order + +import ( + "context" + "errors" + "fmt" +) + +var ErrEnrollmentNotFound = errors.New("enrollment not found") + +const ( + EnrollmentStatusNew = "NEW" // Заказ загружен в систему, но не попал в обработку + EnrollmentStatusProcessing = "PROCESSING" // Вознаграждение за заказ рассчитывается + EnrollmentStatusInvalid = "INVALID" // Система расчёта вознаграждений отказала в расчёте + EnrollmentStatusProcessed = "PROCESSED" // Данные по заказу проверены и информация о расчёте успешно получена +) + +const ( + orderAccrualStatusRegistered = "REGISTERED" + orderAccrualStatusInvalid = "INVALID" + orderAccrualStatusProcessing = "PROCESSING" + orderAccrualStatusProcessed = "PROCESSED" +) + +type OrderAccrualInfo struct { + Order string `json:"order"` + Status string `json:"status"` + Accrual float32 `json:"accrual"` +} + +type EnrollmentController struct { + completeFetchOrderCh chan OrderAccrualInfo + orderServiceAddr string + repository EnrollmentRepository +} + +type Enrollment struct { + UserID int + OrderID string + Status string + Accrual float32 + IsTransferred bool +} + +type EnrollmentRepository interface { + GetByID(ctx context.Context, orderID string) (*Enrollment, error) + CreateNewOrder(ctx context.Context, orderID string) (*Enrollment, error) + ChangeStatus(ctx context.Context, orderID, status string) error +} + +func NewEnrollmentController(orderServiceAddr string, repository EnrollmentRepository) *EnrollmentController { + return &EnrollmentController{orderServiceAddr: orderServiceAddr, repository: repository} // TODO: Create channel handler +} + +func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderNumber string, userID int) (*Enrollment, error) { + if !IsOrderNumberCorrect(orderNumber) { + return nil, ErrInvalidOrderNumber + } + + enrollment, err := controller.repository.GetByID(ctx, orderNumber) + if err != nil && errors.Is(err, ErrEnrollmentNotFound) { + enrollment, err = controller.repository.CreateNewOrder(ctx, orderNumber) + if err != nil { + return nil, fmt.Errorf("error while create new enrollment order: %w", err) + } + } else if err != nil { + return nil, fmt.Errorf("error while require enrollment order: %w", err) + } + + if enrollment.UserID == userID && enrollment.Status == EnrollmentStatusNew { + err = controller.repository.ChangeStatus(ctx, orderNumber, EnrollmentStatusProcessing) + if err != nil { + return nil, fmt.Errorf("error while update order status in require: %w", err) + } + go func(orderNumber string, completeFetchOrderCh chan<- OrderAccrualInfo) { + + // Тут создается клиент и делается запрос на внешний сервис - GET /api/orders/{number} + // Если код ответа 429 - делается повторный запрос после таймера на Retry-After секунд. И так повторяется пока не будет другого ответа + + testAccrualInfo := OrderAccrualInfo{ + Order: orderNumber, + Status: orderAccrualStatusRegistered, + Accrual: 0, + } + + completeFetchOrderCh <- testAccrualInfo + }(orderNumber, controller.completeFetchOrderCh) + } + + return enrollment, nil +} diff --git a/internal/order/order.go b/internal/order/order.go index dde9674..dcfba6f 100644 --- a/internal/order/order.go +++ b/internal/order/order.go @@ -1,6 +1,11 @@ package order -import "strconv" +import ( + "errors" + "strconv" +) + +var ErrInvalidOrderNumber = errors.New("invalid order number") func IsOrderNumberCorrect(orderNumber string) bool { sum := 0 diff --git a/internal/order/pgx-enrollment-repository.go b/internal/order/pgx-enrollment-repository.go new file mode 100644 index 0000000..17364bd --- /dev/null +++ b/internal/order/pgx-enrollment-repository.go @@ -0,0 +1,29 @@ +package order + +import ( + "context" + "database/sql" +) + +type PGXEnrollmentRepository struct { + db *sql.DB +} + +func CreatePGXEnrollmentRepository(db *sql.DB) *PGXEnrollmentRepository { + return &PGXEnrollmentRepository{db: db} +} + +func (P PGXEnrollmentRepository) GetByID(ctx context.Context, orderID string) (*Enrollment, error) { + //TODO implement me + panic("implement me") +} + +func (P PGXEnrollmentRepository) CreateNewOrder(ctx context.Context, orderID string) (*Enrollment, error) { + //TODO implement me + panic("implement me") +} + +func (P PGXEnrollmentRepository) ChangeStatus(ctx context.Context, orderID, status string) error { + //TODO implement me + panic("implement me") +} From 9d6f1437c1b83c70015ac1f33d92845fe6fc5224 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sat, 25 Nov 2023 22:01:08 +0200 Subject: [PATCH 16/29] Fix auth context --- internal/handler/auth-handler.go | 12 +++++++++--- internal/handler/enrollment-order-handler.go | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/internal/handler/auth-handler.go b/internal/handler/auth-handler.go index de19173..97ed5b1 100644 --- a/internal/handler/auth-handler.go +++ b/internal/handler/auth-handler.go @@ -84,7 +84,9 @@ func (handler *AuthHandler) HandleAuthUser(writer http.ResponseWriter, request * http.SetCookie(writer, c) ctx := request.Context() - request.WithContext(context.WithValue(ctx, AuthorizeUserContext, fetchedUser)) + + handler.Logger.Debug("user successful authorized by request", zap.String("user login", fetchedUser.Login)) + request = request.WithContext(context.WithValue(ctx, AuthorizeUserContext, fetchedUser)) writer.WriteHeader(http.StatusOK) } @@ -134,8 +136,10 @@ func (handler *AuthHandler) HandleRegisterUser(writer http.ResponseWriter, reque c := &http.Cookie{Name: authCookieName, Value: sign} http.SetCookie(writer, c) + handler.Logger.Debug("user successful registered by request", zap.String("user login", createdUser.Login)) + ctx := request.Context() - request.WithContext(context.WithValue(ctx, AuthorizeUserContext, createdUser)) + request = request.WithContext(context.WithValue(ctx, AuthorizeUserContext, createdUser)) writer.WriteHeader(http.StatusOK) } @@ -172,7 +176,9 @@ func (handler *AuthHandler) MiddlewareAuthorizeRequest() func(handler http.Handl return } - request.WithContext(context.WithValue(ctx, AuthorizeUserContext, authUser)) + handler.Logger.Debug("user successful authorized", zap.String("user login", authUser.Login)) + + request = request.WithContext(context.WithValue(ctx, AuthorizeUserContext, authUser)) next.ServeHTTP(writer, request) }) diff --git a/internal/handler/enrollment-order-handler.go b/internal/handler/enrollment-order-handler.go index 2fb9699..dc10f57 100644 --- a/internal/handler/enrollment-order-handler.go +++ b/internal/handler/enrollment-order-handler.go @@ -2,6 +2,7 @@ package handler import ( "github.com/nessai1/gophermat/internal/order" + "github.com/nessai1/gophermat/internal/user" "go.uber.org/zap" "net/http" ) @@ -12,7 +13,21 @@ type EnrollmentOrderHandler struct { } func (handler *EnrollmentOrderHandler) HandleLoadOrders(writer http.ResponseWriter, request *http.Request) { + ctxUserVal := request.Context().Value(AuthorizeUserContext) + if ctxUserVal == nil { + handler.Logger.Error("load orders handler must have user in context, but not found") + writer.WriteHeader(http.StatusInternalServerError) + return + } + ctxUser, ok := ctxUserVal.(*user.User) + if !ok { + handler.Logger.Error("cannot cast user in request context while load order") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + writer.Write([]byte(ctxUser.Login)) } func (handler *EnrollmentOrderHandler) HandGetOrders(writer http.ResponseWriter, request *http.Request) { From 38b7969f8527a27da566b64e74076395f6e55b45 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sat, 25 Nov 2023 22:48:32 +0200 Subject: [PATCH 17/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20ID=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/auth-handler.go | 2 +- internal/user/pgx-repository.go | 4 ++-- internal/user/user.go | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/handler/auth-handler.go b/internal/handler/auth-handler.go index 97ed5b1..c9960b5 100644 --- a/internal/handler/auth-handler.go +++ b/internal/handler/auth-handler.go @@ -176,7 +176,7 @@ func (handler *AuthHandler) MiddlewareAuthorizeRequest() func(handler http.Handl return } - handler.Logger.Debug("user successful authorized", zap.String("user login", authUser.Login)) + handler.Logger.Debug("user successful authorized", zap.Int("user id", authUser.ID)) request = request.WithContext(context.WithValue(ctx, AuthorizeUserContext, authUser)) diff --git a/internal/user/pgx-repository.go b/internal/user/pgx-repository.go index de4d0ec..cad3829 100644 --- a/internal/user/pgx-repository.go +++ b/internal/user/pgx-repository.go @@ -20,7 +20,7 @@ func CreatePGXRepository(db *sql.DB) *PGXRepository { } func (repository *PGXRepository) GetUserByLogin(ctx context.Context, login string) (*User, error) { - row := repository.db.QueryRowContext(ctx, "SELECT login, password, balance FROM \"user\" WHERE login = $1", login) + row := repository.db.QueryRowContext(ctx, "SELECT id, login, password, balance FROM \"user\" WHERE login = $1", login) if row.Err() != nil { return nil, fmt.Errorf("error while query for get user by login: %w", row.Err()) @@ -28,7 +28,7 @@ func (repository *PGXRepository) GetUserByLogin(ctx context.Context, login strin var user User - err := row.Scan(&user.Login, &user.password, &user.Balance) + err := row.Scan(&user.ID, &user.Login, &user.password, &user.Balance) if err != nil && errors.Is(err, sql.ErrNoRows) { return nil, ErrUserNotFound } else if err != nil { diff --git a/internal/user/user.go b/internal/user/user.go index 022905e..d3a914c 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -12,6 +12,7 @@ var ErrIncorrectUserPassword = errors.New("user password is wrong") var ErrLoginAlreadyExists = errors.New("input user login already exists") type User struct { + ID int Login string Balance float32 From a7d7fee378eba401f7b5c4ed6913d90e026db78a Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sun, 26 Nov 2023 08:19:58 +0200 Subject: [PATCH 18/29] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0.=20=D0=A7?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/enrollment-order-handler.go | 51 +++++++++++++++++++- internal/order/enrollment.go | 39 +++++++++------ 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/internal/handler/enrollment-order-handler.go b/internal/handler/enrollment-order-handler.go index dc10f57..eacf174 100644 --- a/internal/handler/enrollment-order-handler.go +++ b/internal/handler/enrollment-order-handler.go @@ -1,10 +1,13 @@ package handler import ( + "bytes" + "errors" "github.com/nessai1/gophermat/internal/order" "github.com/nessai1/gophermat/internal/user" - "go.uber.org/zap" "net/http" + + "go.uber.org/zap" ) type EnrollmentOrderHandler struct { @@ -27,7 +30,51 @@ func (handler *EnrollmentOrderHandler) HandleLoadOrders(writer http.ResponseWrit return } - writer.Write([]byte(ctxUser.Login)) + var buffer bytes.Buffer + _, err := buffer.ReadFrom(request.Body) + if err != nil { + handler.Logger.Debug("user sends invalid request") + writer.WriteHeader(http.StatusBadRequest) + return + } + + orderNumber := buffer.String() + enrollmentOrder, err := handler.EnrollmentController.RequireOrder(request.Context(), orderNumber, ctxUser.ID) + if err != nil && errors.Is(err, order.ErrInvalidOrderNumber) { + handler.Logger.Debug("user register order number with invalid format", zap.String("order number", orderNumber)) + writer.WriteHeader(http.StatusUnprocessableEntity) + return + } + + if enrollmentOrder.UserID != ctxUser.ID { + handler.Logger.Debug( + "user register someone else's order", + zap.String("order number", orderNumber), + zap.Int("request user id", ctxUser.ID), + zap.Int("order owner user id", ctxUser.ID), + ) + + writer.WriteHeader(http.StatusConflict) + return + } + + if enrollmentOrder.Status == order.EnrollmentStatusNew { + handler.Logger.Debug( + "user successful load new order", + zap.String("order number", orderNumber), + zap.Int("user id", ctxUser.ID), + ) + writer.WriteHeader(http.StatusAccepted) + return + } + + handler.Logger.Debug( + "user try to load already exists own order", + zap.String("order number", orderNumber), + zap.Int("user id", ctxUser.ID), + ) + + writer.WriteHeader(http.StatusOK) } func (handler *EnrollmentOrderHandler) HandGetOrders(writer http.ResponseWriter, request *http.Request) { diff --git a/internal/order/enrollment.go b/internal/order/enrollment.go index f94baae..a4b48fb 100644 --- a/internal/order/enrollment.go +++ b/internal/order/enrollment.go @@ -15,6 +15,7 @@ const ( EnrollmentStatusProcessed = "PROCESSED" // Данные по заказу проверены и информация о расчёте успешно получена ) +// Статусы внешнего сервиса const ( orderAccrualStatusRegistered = "REGISTERED" orderAccrualStatusInvalid = "INVALID" @@ -68,24 +69,34 @@ func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderN } if enrollment.UserID == userID && enrollment.Status == EnrollmentStatusNew { - err = controller.repository.ChangeStatus(ctx, orderNumber, EnrollmentStatusProcessing) + err = controller.LoadOrder(ctx, enrollment.OrderID) if err != nil { - return nil, fmt.Errorf("error while update order status in require: %w", err) + return nil, fmt.Errorf("error while start order loading operation: %w", err) } - go func(orderNumber string, completeFetchOrderCh chan<- OrderAccrualInfo) { - - // Тут создается клиент и делается запрос на внешний сервис - GET /api/orders/{number} - // Если код ответа 429 - делается повторный запрос после таймера на Retry-After секунд. И так повторяется пока не будет другого ответа + } - testAccrualInfo := OrderAccrualInfo{ - Order: orderNumber, - Status: orderAccrualStatusRegistered, - Accrual: 0, - } + return enrollment, nil +} - completeFetchOrderCh <- testAccrualInfo - }(orderNumber, controller.completeFetchOrderCh) +func (controller *EnrollmentController) LoadOrder(ctx context.Context, orderNumber string) error { + err := controller.repository.ChangeStatus(ctx, orderNumber, EnrollmentStatusProcessing) + if err != nil { + return fmt.Errorf("error while update order status in require: %w", err) } - return enrollment, nil + go func(orderNumber string, completeFetchOrderCh chan<- OrderAccrualInfo) { + + // Тут создается клиент и делается запрос на внешний сервис - GET /api/orders/{number} + // Если код ответа 429 - делается повторный запрос после таймера на Retry-After секунд. И так повторяется пока не будет другого ответа + + testAccrualInfo := OrderAccrualInfo{ + Order: orderNumber, + Status: orderAccrualStatusRegistered, + Accrual: 0, + } + + completeFetchOrderCh <- testAccrualInfo + }(orderNumber, controller.completeFetchOrderCh) + + return nil } From eecfc2e5c32d0e425dbace772ecf8e6379dd28d5 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 27 Nov 2023 04:49:15 +0300 Subject: [PATCH 19/29] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/config.go | 21 +++---- internal/user/user.go | 44 ++++++++++++- internal/user/user_test.go | 84 ++++++++++++++++++++++++- migrations/001_create_user_table.up.sql | 2 +- 4 files changed, 134 insertions(+), 17 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index f7012e4..6bf316e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,17 +2,17 @@ package config import ( "flag" - "os" - "sync" - "github.com/joho/godotenv" + "os" ) type EnvType string -const EnvTypeDevelopment EnvType = "development" -const EnvTypeStage EnvType = "stage" -const EnvTypeProduction EnvType = "production" +const ( + EnvTypeDevelopment EnvType = "development" + EnvTypeStage EnvType = "stage" + EnvTypeProduction EnvType = "production" +) type Config struct { ServiceAddr string @@ -22,15 +22,8 @@ type Config struct { EnvType EnvType } -var config *Config -var once sync.Once - func GetConfig() *Config { - once.Do(func() { - config = fetchConfig() - }) - - return config + return fetchConfig() } func fetchConfig() *Config { diff --git a/internal/user/user.go b/internal/user/user.go index d3a914c..2d91be4 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -5,6 +5,8 @@ import ( "crypto/sha256" "errors" "fmt" + "strconv" + "strings" ) var ErrUserNotFound = errors.New("user not found") @@ -14,14 +16,49 @@ var ErrLoginAlreadyExists = errors.New("input user login already exists") type User struct { ID int Login string - Balance float32 + Balance int64 password string } +func parseBalance(balance string) (int64, error) { + parts := strings.Split(balance, ".") + + if len(parts) > 2 { + return 0, fmt.Errorf("parts of balance must be less than 3, got %d", len(parts)) + } + + bigPart, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, err + } + + val := int64(bigPart * 100) + + if len(parts) == 2 { + smallPart, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, err + } + + if smallPart >= 100 { + return 0, fmt.Errorf("small part must be less than 100") + } + + if smallPart < 10 { + val += int64(smallPart * 10) + } else { + val += int64(smallPart) + } + } + + return val, nil +} + type Repository interface { GetUserByLogin(context.Context, string) (*User, error) CreateUser(context.Context, *User) error + //AddBalanceByID(context.Context, int, int) error } type Controller struct { @@ -73,6 +110,11 @@ func (controller *Controller) AddUser(ctx context.Context, login, password strin return &user, nil } +// +//func (controller *Controller) AddBalanceByID(ctx context.Context, userID int, balance float32) error { +// return controller.repository.AddBalanceByID(ctx, userID, balance) +//} + func buildPasswordHash(password string) string { shaSum := sha256.Sum256([]byte(password)) diff --git a/internal/user/user_test.go b/internal/user/user_test.go index 1f15c1c..d4b5f3f 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -11,7 +11,7 @@ import ( func TestController_GetUserByCredentials(t *testing.T) { userOnePassword := "superSecret" userOneLogin := "userOne" - userOneBalance := float32(300) + userOneBalance := int64(300) users := map[string]User{ userOneLogin: { Login: userOneLogin, @@ -85,3 +85,85 @@ func Test_buildPasswordHash(t *testing.T) { h = buildPasswordHash(superSecret) // second check for invariance assert.Truef(t, h == superSecretHash, "hash must be equal (%s == %s)", h, superSecretHash) } + +func TestParseBalance(t *testing.T) { + tests := []struct { + name string + hasError bool + in string + out int64 + }{ + { + name: "Correct val 1", + hasError: false, + in: "42.10", + out: 4210, + }, + { + name: "Correct val 2", + hasError: false, + in: "42.7", + out: 4270, + }, + { + name: "Correct val 3", + hasError: false, + in: "42", + out: 4200, + }, + { + name: "Incorrect val 1", + hasError: true, + in: "foo", + out: 0, + }, + { + name: "Incorrect val 2", + hasError: true, + in: "foo.bar", + out: 0, + }, + { + name: "Incorrect val 3", + hasError: true, + in: "54.ad", + out: 0, + }, + { + name: "Incorrect val 4", + hasError: true, + in: "56.100", + out: 0, + }, + { + name: "Incorrect val 5", + hasError: true, + in: "fa.42", + out: 0, + }, + { + name: "Incorrect val 6", + hasError: true, + in: "23a.4bd", + out: 0, + }, + { + name: "Incorrect val 7", + hasError: true, + in: "65.32.22", + out: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := parseBalance(tt.in) + assert.Equal(t, tt.out, val) + if tt.hasError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/migrations/001_create_user_table.up.sql b/migrations/001_create_user_table.up.sql index a666a04..894b31e 100644 --- a/migrations/001_create_user_table.up.sql +++ b/migrations/001_create_user_table.up.sql @@ -2,5 +2,5 @@ CREATE TABLE IF NOT EXISTS "user" ( id serial primary key, login varchar(255) not null unique, password varchar(255) not null, - balance real not null default 0.0 + balance bigint not null default 0 ) \ No newline at end of file From cefbff18866ad3774826c989b744a1d0d393ba2e Mon Sep 17 00:00:00 2001 From: nessai1 Date: Thu, 30 Nov 2023 09:40:24 +0200 Subject: [PATCH 20/29] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gophermart/gophermart.go | 7 +- internal/handler/enrollment-order-handler.go | 18 +++- internal/order/enrollment.go | 98 +++++++++++++------ internal/order/pgx-enrollment-repository.go | 55 +++++++++-- internal/postgrescodes/postgrescodes.go | 3 + internal/user/map-repository.go | 20 ++++ internal/user/pgx-repository.go | 29 +++++- internal/user/user.go | 14 ++- internal/user/user_test.go | 2 +- .../002_create_enrollment_order_table.up.sql | 6 +- 10 files changed, 201 insertions(+), 51 deletions(-) create mode 100644 internal/postgrescodes/postgrescodes.go diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index c247f5c..94a7513 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -28,9 +28,10 @@ func Start() error { return fmt.Errorf("cannot initialize database on start service: %w", err) } + userController := user.NewController(user.CreatePGXRepository(db)) authHandler := handler.AuthHandler{ Logger: log, - UserController: user.NewController(user.CreatePGXRepository(db)), + UserController: userController, } authMux := chi.NewMux() @@ -39,12 +40,12 @@ func Start() error { enrollmentController := handler.EnrollmentOrderHandler{ Logger: log, - EnrollmentController: order.NewEnrollmentController(cfg.AccrualServiceAddr, order.CreatePGXEnrollmentRepository(db)), + EnrollmentController: order.NewEnrollmentController(cfg.AccrualServiceAddr, order.CreatePGXEnrollmentRepository(db), userController), } enrollmentMux := chi.NewMux() enrollmentMux.Use(authHandler.MiddlewareAuthorizeRequest()) enrollmentMux.Post("/", enrollmentController.HandleLoadOrders) - enrollmentMux.Get("/", enrollmentController.HandGetOrders) + enrollmentMux.Get("/", enrollmentController.HandleGetOrders) balanceController := handler.BalanceHandler{ Logger: log, diff --git a/internal/handler/enrollment-order-handler.go b/internal/handler/enrollment-order-handler.go index eacf174..e5a14f4 100644 --- a/internal/handler/enrollment-order-handler.go +++ b/internal/handler/enrollment-order-handler.go @@ -77,6 +77,20 @@ func (handler *EnrollmentOrderHandler) HandleLoadOrders(writer http.ResponseWrit writer.WriteHeader(http.StatusOK) } -func (handler *EnrollmentOrderHandler) HandGetOrders(writer http.ResponseWriter, request *http.Request) { - writer.Write([]byte("Getting orders....")) +func (handler *EnrollmentOrderHandler) HandleGetOrders(writer http.ResponseWriter, request *http.Request) { + //ctxUserVal := request.Context().Value(AuthorizeUserContext) + //if ctxUserVal == nil { + // handler.Logger.Error("load orders handler must have user in context, but not found") + // writer.WriteHeader(http.StatusInternalServerError) + // return + //} + // + //ctxUser, ok := ctxUserVal.(*user.User) + //if !ok { + // handler.Logger.Error("cannot cast user in request context while load order") + // writer.WriteHeader(http.StatusInternalServerError) + // return + //} + // + //handler.EnrollmentController.GetUserOrderListByID(ctxUser.ID) } diff --git a/internal/order/enrollment.go b/internal/order/enrollment.go index a4b48fb..6878211 100644 --- a/internal/order/enrollment.go +++ b/internal/order/enrollment.go @@ -1,12 +1,19 @@ package order import ( + "bytes" "context" + "encoding/json" "errors" "fmt" + "github.com/nessai1/gophermat/internal/user" + "net/http" + "strconv" + "time" ) var ErrEnrollmentNotFound = errors.New("enrollment not found") +var ErrEnrollmentAlreadyExists = errors.New("enrollment already exists") const ( EnrollmentStatusNew = "NEW" // Заказ загружен в систему, но не попал в обработку @@ -24,33 +31,32 @@ const ( ) type OrderAccrualInfo struct { - Order string `json:"order"` - Status string `json:"status"` - Accrual float32 `json:"accrual"` + Order string `json:"order"` + Status string `json:"status"` + Accrual json.Number `json:"accrual"` } type EnrollmentController struct { - completeFetchOrderCh chan OrderAccrualInfo - orderServiceAddr string - repository EnrollmentRepository + orderServiceAddr string + repository EnrollmentRepository + userController *user.Controller } type Enrollment struct { - UserID int - OrderID string - Status string - Accrual float32 - IsTransferred bool + UserID int + OrderID string + Status string + Accrual int64 } type EnrollmentRepository interface { GetByID(ctx context.Context, orderID string) (*Enrollment, error) - CreateNewOrder(ctx context.Context, orderID string) (*Enrollment, error) + CreateNewOrder(ctx context.Context, orderID string, ownerID int) (*Enrollment, error) ChangeStatus(ctx context.Context, orderID, status string) error } -func NewEnrollmentController(orderServiceAddr string, repository EnrollmentRepository) *EnrollmentController { - return &EnrollmentController{orderServiceAddr: orderServiceAddr, repository: repository} // TODO: Create channel handler +func NewEnrollmentController(orderServiceAddr string, repository EnrollmentRepository, userController *user.Controller) *EnrollmentController { + return &EnrollmentController{orderServiceAddr: orderServiceAddr, repository: repository, userController: userController} // TODO: Create channel handler } func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderNumber string, userID int) (*Enrollment, error) { @@ -60,7 +66,7 @@ func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderN enrollment, err := controller.repository.GetByID(ctx, orderNumber) if err != nil && errors.Is(err, ErrEnrollmentNotFound) { - enrollment, err = controller.repository.CreateNewOrder(ctx, orderNumber) + enrollment, err = controller.repository.CreateNewOrder(ctx, orderNumber, userID) if err != nil { return nil, fmt.Errorf("error while create new enrollment order: %w", err) } @@ -69,7 +75,7 @@ func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderN } if enrollment.UserID == userID && enrollment.Status == EnrollmentStatusNew { - err = controller.LoadOrder(ctx, enrollment.OrderID) + err = controller.LoadOrder(ctx, userID, enrollment.OrderID) if err != nil { return nil, fmt.Errorf("error while start order loading operation: %w", err) } @@ -78,25 +84,61 @@ func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderN return enrollment, nil } -func (controller *EnrollmentController) LoadOrder(ctx context.Context, orderNumber string) error { +func (controller *EnrollmentController) LoadOrder(ctx context.Context, ownerID int, orderNumber string) error { err := controller.repository.ChangeStatus(ctx, orderNumber, EnrollmentStatusProcessing) if err != nil { return fmt.Errorf("error while update order status in require: %w", err) } - go func(orderNumber string, completeFetchOrderCh chan<- OrderAccrualInfo) { - - // Тут создается клиент и делается запрос на внешний сервис - GET /api/orders/{number} - // Если код ответа 429 - делается повторный запрос после таймера на Retry-After секунд. И так повторяется пока не будет другого ответа - - testAccrualInfo := OrderAccrualInfo{ - Order: orderNumber, - Status: orderAccrualStatusRegistered, - Accrual: 0, + go func(serviceAddr, orderNumber string, ownerID int, enrollmentRepository EnrollmentRepository, userController *user.Controller) { + for { + resp, err := http.Get(serviceAddr + "/api/orders/" + orderNumber) + if err != nil { + break + } + + if resp.StatusCode == http.StatusTooManyRequests { + retryAfter := resp.Header.Get("Retry-After") + retryAfterInt, _ := strconv.Atoi(retryAfter) + time.Sleep(time.Second * time.Duration(retryAfterInt)) + continue + } + + if resp.StatusCode != http.StatusOK { + break + } + + var buffer bytes.Buffer + buffer.ReadFrom(resp.Body) + var accrualInfo OrderAccrualInfo + json.Unmarshal(buffer.Bytes(), &accrualInfo) + if accrualInfo.Status == orderAccrualStatusInvalid { + enrollmentRepository.ChangeStatus(context.TODO(), orderNumber, EnrollmentStatusInvalid) + return + } + + if accrualInfo.Status == orderAccrualStatusProcessing || accrualInfo.Status == orderAccrualStatusRegistered { + time.Sleep(time.Second * 5) + continue + } + + enrollmentRepository.ChangeStatus(context.TODO(), orderNumber, EnrollmentStatusProcessed) + owner, err := userController.GetUserByID(context.TODO(), ownerID) + if err != nil { + return + } + + df, err := user.ParseBalance(string(accrualInfo.Accrual)) + if err != nil { + return + } + + balance := owner.Balance + df + userController.SetUserBalanceByID(context.TODO(), ownerID, balance) + return } - completeFetchOrderCh <- testAccrualInfo - }(orderNumber, controller.completeFetchOrderCh) + }(controller.orderServiceAddr, orderNumber, ownerID, controller.repository, controller.userController) return nil } diff --git a/internal/order/pgx-enrollment-repository.go b/internal/order/pgx-enrollment-repository.go index 17364bd..9da1749 100644 --- a/internal/order/pgx-enrollment-repository.go +++ b/internal/order/pgx-enrollment-repository.go @@ -3,6 +3,10 @@ package order import ( "context" "database/sql" + "errors" + "fmt" + "github.com/jackc/pgx/v5/pgconn" + "github.com/nessai1/gophermat/internal/postgrescodes" ) type PGXEnrollmentRepository struct { @@ -13,17 +17,50 @@ func CreatePGXEnrollmentRepository(db *sql.DB) *PGXEnrollmentRepository { return &PGXEnrollmentRepository{db: db} } -func (P PGXEnrollmentRepository) GetByID(ctx context.Context, orderID string) (*Enrollment, error) { - //TODO implement me - panic("implement me") +func (repository *PGXEnrollmentRepository) GetByID(ctx context.Context, orderID string) (*Enrollment, error) { + row := repository.db.QueryRowContext(ctx, "SELECT user_id, order_id, status, accrual FROM enrollment_order WHERE order_id = $1", orderID) + + if row.Err() != nil { + return nil, fmt.Errorf("error while query for get user by login: %w", row.Err()) + } + + var enrollment Enrollment + + err := row.Scan(&enrollment.UserID, &enrollment.OrderID, &enrollment.Status, &enrollment.Accrual) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, ErrEnrollmentNotFound + } else if err != nil { + return nil, fmt.Errorf("error while scan row for get enrollment by login: %w", err) + } + + return &enrollment, nil } -func (P PGXEnrollmentRepository) CreateNewOrder(ctx context.Context, orderID string) (*Enrollment, error) { - //TODO implement me - panic("implement me") +func (repository *PGXEnrollmentRepository) CreateNewOrder(ctx context.Context, orderID string, ownerID int) (*Enrollment, error) { + _, err := repository.db.ExecContext(ctx, "INSERT INTO enrollment_order (order_id, user_id, status) VALUES ($1, $2, $3)", orderID, ownerID, EnrollmentStatusNew) + + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + if pgErr.Code == postgrescodes.PostgresErrCodeUniqueViolation { + return nil, ErrEnrollmentAlreadyExists + } + } + + return nil, fmt.Errorf("error while creating user: %w", err) + } + + enrollment := Enrollment{ + UserID: ownerID, + OrderID: orderID, + Status: EnrollmentStatusNew, + Accrual: 0, + } + + return &enrollment, nil } -func (P PGXEnrollmentRepository) ChangeStatus(ctx context.Context, orderID, status string) error { - //TODO implement me - panic("implement me") +func (repository *PGXEnrollmentRepository) ChangeStatus(ctx context.Context, orderID, status string) error { + _, err := repository.db.ExecContext(ctx, "UPDATE enrollment_order SET status = $1 WHERE order_id = $2", status, orderID) + return err } diff --git a/internal/postgrescodes/postgrescodes.go b/internal/postgrescodes/postgrescodes.go new file mode 100644 index 0000000..aa9a878 --- /dev/null +++ b/internal/postgrescodes/postgrescodes.go @@ -0,0 +1,3 @@ +package postgrescodes + +const PostgresErrCodeUniqueViolation = "23505" diff --git a/internal/user/map-repository.go b/internal/user/map-repository.go index d77bcd6..c3e4774 100644 --- a/internal/user/map-repository.go +++ b/internal/user/map-repository.go @@ -25,3 +25,23 @@ func (repository *MapRepository) CreateUser(_ context.Context, user *User) error repository.data[user.Login] = *user return nil } + +func (repository *MapRepository) SetUserBalanceByID(_ context.Context, userID int, balance int64) error { + for _, user := range repository.data { + if user.ID == userID { + user.Balance = balance + } + } + + return ErrUserNotFound +} + +func (repository *MapRepository) GetUserByID(ctx context.Context, userID int) (*User, error) { + for _, user := range repository.data { + if user.ID == userID { + return &user, nil + } + } + + return nil, ErrUserNotFound +} diff --git a/internal/user/pgx-repository.go b/internal/user/pgx-repository.go index cad3829..136bb8a 100644 --- a/internal/user/pgx-repository.go +++ b/internal/user/pgx-repository.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/nessai1/gophermat/internal/postgrescodes" "github.com/jackc/pgx/v5/pgconn" ) @@ -13,8 +14,6 @@ type PGXRepository struct { db *sql.DB } -const PostgresErrCodeUniqueViolation = "23505" - func CreatePGXRepository(db *sql.DB) *PGXRepository { return &PGXRepository{db: db} } @@ -44,7 +43,7 @@ func (repository *PGXRepository) CreateUser(ctx context.Context, user *User) err if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { - if pgErr.Code == PostgresErrCodeUniqueViolation { + if pgErr.Code == postgrescodes.PostgresErrCodeUniqueViolation { return ErrLoginAlreadyExists } } @@ -54,3 +53,27 @@ func (repository *PGXRepository) CreateUser(ctx context.Context, user *User) err return nil } + +func (repository *PGXRepository) GetUserByID(ctx context.Context, id int) (*User, error) { + row := repository.db.QueryRowContext(ctx, "SELECT id, login, password, balance FROM \"user\" WHERE id = $1", id) + + if row.Err() != nil { + return nil, fmt.Errorf("error while query for get user by id: %w", row.Err()) + } + + var user User + + err := row.Scan(&user.ID, &user.Login, &user.password, &user.Balance) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, ErrUserNotFound + } else if err != nil { + return nil, fmt.Errorf("error while scan row for get user by id: %w", err) + } + + return &user, nil +} + +func (repository *PGXRepository) SetUserBalanceByID(ctx context.Context, userID int, balance int64) error { + _, err := repository.db.ExecContext(ctx, "UPDATE \"user\" SET balance = $1 WHERE id = $2", balance, userID) + return err +} diff --git a/internal/user/user.go b/internal/user/user.go index 2d91be4..0ee9b92 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -21,7 +21,7 @@ type User struct { password string } -func parseBalance(balance string) (int64, error) { +func ParseBalance(balance string) (int64, error) { parts := strings.Split(balance, ".") if len(parts) > 2 { @@ -58,7 +58,8 @@ func parseBalance(balance string) (int64, error) { type Repository interface { GetUserByLogin(context.Context, string) (*User, error) CreateUser(context.Context, *User) error - //AddBalanceByID(context.Context, int, int) error + GetUserByID(context.Context, int) (*User, error) + SetUserBalanceByID(context.Context, int, int64) error } type Controller struct { @@ -90,6 +91,11 @@ func (controller *Controller) GetUserByLogin(ctx context.Context, login string) return user, err } +func (controller *Controller) GetUserByID(ctx context.Context, id int) (*User, error) { + user, err := controller.repository.GetUserByID(ctx, id) + return user, err +} + func (controller *Controller) AddUser(ctx context.Context, login, password string) (*User, error) { passwordHash := buildPasswordHash(password) @@ -110,6 +116,10 @@ func (controller *Controller) AddUser(ctx context.Context, login, password strin return &user, nil } +func (controller *Controller) SetUserBalanceByID(ctx context.Context, userID int, balance int64) error { + return controller.repository.SetUserBalanceByID(ctx, userID, balance) +} + // //func (controller *Controller) AddBalanceByID(ctx context.Context, userID int, balance float32) error { // return controller.repository.AddBalanceByID(ctx, userID, balance) diff --git a/internal/user/user_test.go b/internal/user/user_test.go index d4b5f3f..926df77 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -157,7 +157,7 @@ func TestParseBalance(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - val, err := parseBalance(tt.in) + val, err := ParseBalance(tt.in) assert.Equal(t, tt.out, val) if tt.hasError { assert.Error(t, err) diff --git a/migrations/002_create_enrollment_order_table.up.sql b/migrations/002_create_enrollment_order_table.up.sql index 282f7da..4923332 100644 --- a/migrations/002_create_enrollment_order_table.up.sql +++ b/migrations/002_create_enrollment_order_table.up.sql @@ -1,10 +1,10 @@ BEGIN; CREATE TABLE enrollment_order ( - order_id char(16) not null PRIMARY KEY, + order_id varchar(16) not null PRIMARY KEY, user_id int not null references "user" (id), status varchar(30) not null, - accrual real not null default 0.0, - is_accrual_transferred bool not null default false + accrual bigint not null default 0, + uploaded_at timestamp not null default now() ); CREATE INDEX enrollment_order_user_id_idx ON enrollment_order (user_id); COMMIT; \ No newline at end of file From 9b7812cbe8eeb8bb1f43ef2d7f345bc42df42472 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sun, 3 Dec 2023 09:30:42 +0200 Subject: [PATCH 21/29] =?UTF-8?q?=D0=9F=D0=BE=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D1=8C=20accrual?= =?UTF-8?q?=20=D0=B2=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/enrollment-order-handler.go | 72 ++++++++++++++++---- internal/order/enrollment.go | 19 ++++-- internal/order/pgx-enrollment-repository.go | 31 +++++++++ 3 files changed, 103 insertions(+), 19 deletions(-) diff --git a/internal/handler/enrollment-order-handler.go b/internal/handler/enrollment-order-handler.go index e5a14f4..93dd75e 100644 --- a/internal/handler/enrollment-order-handler.go +++ b/internal/handler/enrollment-order-handler.go @@ -2,10 +2,12 @@ package handler import ( "bytes" + "encoding/json" "errors" "github.com/nessai1/gophermat/internal/order" "github.com/nessai1/gophermat/internal/user" "net/http" + "time" "go.uber.org/zap" ) @@ -15,6 +17,13 @@ type EnrollmentOrderHandler struct { EnrollmentController *order.EnrollmentController } +type EnrollmentItem struct { + OrderID string `json:"number"` + Status string `json:"status"` + Accrual float32 `json:"accrual"` + UploadedAt time.Time `json:"uploaded_at"` +} + func (handler *EnrollmentOrderHandler) HandleLoadOrders(writer http.ResponseWriter, request *http.Request) { ctxUserVal := request.Context().Value(AuthorizeUserContext) if ctxUserVal == nil { @@ -78,19 +87,52 @@ func (handler *EnrollmentOrderHandler) HandleLoadOrders(writer http.ResponseWrit } func (handler *EnrollmentOrderHandler) HandleGetOrders(writer http.ResponseWriter, request *http.Request) { - //ctxUserVal := request.Context().Value(AuthorizeUserContext) - //if ctxUserVal == nil { - // handler.Logger.Error("load orders handler must have user in context, but not found") - // writer.WriteHeader(http.StatusInternalServerError) - // return - //} - // - //ctxUser, ok := ctxUserVal.(*user.User) - //if !ok { - // handler.Logger.Error("cannot cast user in request context while load order") - // writer.WriteHeader(http.StatusInternalServerError) - // return - //} - // - //handler.EnrollmentController.GetUserOrderListByID(ctxUser.ID) + ctxUserVal := request.Context().Value(AuthorizeUserContext) + if ctxUserVal == nil { + handler.Logger.Error("load orders handler must have user in context, but not found") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + ctxUser, ok := ctxUserVal.(*user.User) + if !ok { + handler.Logger.Error("cannot cast user in request context while load order") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + enrollmentList, err := handler.EnrollmentController.GetUserOrderListByID(request.Context(), ctxUser.ID) + if err != nil { + handler.Logger.Error("error while get enrollment list for user", zap.Int("user id", ctxUser.ID), zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + } + + if len(enrollmentList) == 0 { + writer.WriteHeader(http.StatusNoContent) + } + + resultEnrollmentList := make([]EnrollmentItem, len(enrollmentList)) + for i := 0; i < len(enrollmentList); i++ { + item := EnrollmentItem{ + OrderID: enrollmentList[i].OrderID, + Status: enrollmentList[i].Status, + Accrual: float32(enrollmentList[i].Accrual) / 100, + UploadedAt: enrollmentList[i].UploadedAt, + } + + resultEnrollmentList[i] = item + } + + rs, err := json.Marshal(resultEnrollmentList) + if err != nil { + handler.Logger.Error("error while marshal list of user enrollments", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + _, err = writer.Write(rs) + if err != nil { + handler.Logger.Error("error while write enrollment list to result body", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + } } diff --git a/internal/order/enrollment.go b/internal/order/enrollment.go index 6878211..be77d5c 100644 --- a/internal/order/enrollment.go +++ b/internal/order/enrollment.go @@ -43,16 +43,19 @@ type EnrollmentController struct { } type Enrollment struct { - UserID int - OrderID string - Status string - Accrual int64 + UserID int + OrderID string + Status string + Accrual int64 + UploadedAt time.Time } type EnrollmentRepository interface { GetByID(ctx context.Context, orderID string) (*Enrollment, error) CreateNewOrder(ctx context.Context, orderID string, ownerID int) (*Enrollment, error) ChangeStatus(ctx context.Context, orderID, status string) error + UpdateOrderAccrual(ctx context.Context, orderID string, accrual int) error + GetListByUserID(ctx context.Context, userID int) ([]*Enrollment, error) } func NewEnrollmentController(orderServiceAddr string, repository EnrollmentRepository, userController *user.Controller) *EnrollmentController { @@ -133,6 +136,8 @@ func (controller *EnrollmentController) LoadOrder(ctx context.Context, ownerID i return } + enrollmentRepository.UpdateOrderAccrual(context.TODO(), orderNumber, int(df)) + balance := owner.Balance + df userController.SetUserBalanceByID(context.TODO(), ownerID, balance) return @@ -142,3 +147,9 @@ func (controller *EnrollmentController) LoadOrder(ctx context.Context, ownerID i return nil } + +func (controller *EnrollmentController) GetUserOrderListByID(ctx context.Context, userID int) ([]*Enrollment, error) { + enrollmentList, err := controller.repository.GetListByUserID(ctx, userID) + + return enrollmentList, err +} diff --git a/internal/order/pgx-enrollment-repository.go b/internal/order/pgx-enrollment-repository.go index 9da1749..b9e4f94 100644 --- a/internal/order/pgx-enrollment-repository.go +++ b/internal/order/pgx-enrollment-repository.go @@ -64,3 +64,34 @@ func (repository *PGXEnrollmentRepository) ChangeStatus(ctx context.Context, ord _, err := repository.db.ExecContext(ctx, "UPDATE enrollment_order SET status = $1 WHERE order_id = $2", status, orderID) return err } + +func (repository *PGXEnrollmentRepository) UpdateOrderAccrual(ctx context.Context, orderID string, accrual int) error { + _, err := repository.db.ExecContext(ctx, "UPDATE enrollment_order SET accrual = $1 WHERE order_id = $2", accrual, orderID) + return err +} + +func (repository *PGXEnrollmentRepository) GetListByUserID(ctx context.Context, userID int) ([]*Enrollment, error) { + rows, err := repository.db.QueryContext(ctx, "SELECT order_id, status, accrual, uploaded_at FROM enrollment_order WHERE user_id = $1 ORDER BY uploaded_at ASC", userID) + if err != nil { + return nil, err + } + defer rows.Close() + + enrollmentList := make([]*Enrollment, 0) + + for rows.Next() { + if err = rows.Err(); err != nil { + return nil, err + } + + enrollment := Enrollment{UserID: userID} + err = rows.Scan(&enrollment.OrderID, &enrollment.Status, &enrollment.Accrual, &enrollment.UploadedAt) + if err != nil { + return nil, err + } + + enrollmentList = append(enrollmentList, &enrollment) + } + + return enrollmentList, nil +} From 131c00aa69c4e73b033ce7ff467fc8aa3d7853d0 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sun, 3 Dec 2023 10:05:54 +0200 Subject: [PATCH 22/29] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BB=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D0=B4=20accrual=20=D0=B2=20=D1=81=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B5=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B5=D1=81=D0=BB=D0=B8=20=D1=81=D1=82=D0=BE=D0=B8=D1=82=20?= =?UTF-8?q?=D0=B4=D0=B5=D1=84=D0=BE=D0=BB=D1=82=D0=BD=D0=BE=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/enrollment-order-handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/enrollment-order-handler.go b/internal/handler/enrollment-order-handler.go index 93dd75e..65d1d35 100644 --- a/internal/handler/enrollment-order-handler.go +++ b/internal/handler/enrollment-order-handler.go @@ -20,7 +20,7 @@ type EnrollmentOrderHandler struct { type EnrollmentItem struct { OrderID string `json:"number"` Status string `json:"status"` - Accrual float32 `json:"accrual"` + Accrual float32 `json:"accrual,omitempty"` UploadedAt time.Time `json:"uploaded_at"` } From c05d093458d09fb9564416c5aac37919988d2ff1 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sun, 3 Dec 2023 19:41:06 +0200 Subject: [PATCH 23/29] =?UTF-8?q?=D0=98=D1=81=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=BE=D0=B2=20(withdraw?= =?UTF-8?q?als)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev/sql_scheme.drawio | 2 +- internal/gophermart/gophermart.go | 12 +- internal/handler/balance-handler.go | 173 +++++++++++++++++- internal/order/enrollment.go | 5 +- internal/order/order.go | 9 + internal/order/pgx-withdraw-repository.go | 74 ++++++++ internal/order/withdraw.go | 72 ++++++++ .../002_create_enrollment_order_table.up.sql | 2 +- .../003_create_withdraw_order_table.down.sql | 4 + .../003_create_withdraw_order_table.up.sql | 10 + 10 files changed, 353 insertions(+), 10 deletions(-) create mode 100644 internal/order/pgx-withdraw-repository.go create mode 100644 internal/order/withdraw.go create mode 100644 migrations/003_create_withdraw_order_table.down.sql create mode 100644 migrations/003_create_withdraw_order_table.up.sql diff --git a/dev/sql_scheme.drawio b/dev/sql_scheme.drawio index 59037b1..92bf67a 100644 --- a/dev/sql_scheme.drawio +++ b/dev/sql_scheme.drawio @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index 94a7513..ef75e17 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -48,17 +48,23 @@ func Start() error { enrollmentMux.Get("/", enrollmentController.HandleGetOrders) balanceController := handler.BalanceHandler{ - Logger: log, + Logger: log, + WithdrawController: order.NewWithdrawController(order.NewPGXWithdrawRepository(db), userController), } + balanceMux := chi.NewMux() balanceMux.Use(authHandler.MiddlewareAuthorizeRequest()) balanceMux.Get("/", balanceController.HandleGetBalance) - balanceMux.Post("/withdraw", balanceController.HandleWithdraw) + balanceMux.Post("/withdraw", balanceController.HandleAddWithdraw) + + withdrawInfoMux := chi.NewMux() + withdrawInfoMux.Use(authHandler.MiddlewareAuthorizeRequest()) + withdrawInfoMux.Get("/", balanceController.HandleGetListWithdraw) router.Mount("/", authMux) router.Mount("/api/user/orders", enrollmentMux) router.Mount("/api/user/balance", balanceMux) - // TODO: add /api/user/withdrawals handle + router.Mount("/api/user/withdrawals", withdrawInfoMux) log.Info("starting service", zap.String("service address", cfg.ServiceAddr)) diff --git a/internal/handler/balance-handler.go b/internal/handler/balance-handler.go index 0058454..dc62c90 100644 --- a/internal/handler/balance-handler.go +++ b/internal/handler/balance-handler.go @@ -1,18 +1,183 @@ package handler import ( + "bytes" + "encoding/json" + "errors" + "github.com/nessai1/gophermat/internal/order" + "github.com/nessai1/gophermat/internal/user" "go.uber.org/zap" "net/http" + "time" ) type BalanceHandler struct { - Logger *zap.Logger + Logger *zap.Logger + WithdrawController *order.WithdrawController +} + +type AddWithdrawRequest struct { + Order string `json:"order"` + Sum json.Number `json:"sum"` +} + +type GetWithdrawResponse struct { + Current float32 `json:"current"` + Withdrawn float32 `json:"withdrawn"` +} + +type GetWithdrawItemResponse struct { + Order string `json:"order"` + Sum float32 `json:"sum"` + ProcessedAt time.Time `json:"processed_at"` } func (handler *BalanceHandler) HandleGetBalance(writer http.ResponseWriter, request *http.Request) { - writer.Write([]byte("Getting balance....")) + ctxUserVal := request.Context().Value(AuthorizeUserContext) + if ctxUserVal == nil { + handler.Logger.Error("load orders handler must have user in context, but not found") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + ctxUser, ok := ctxUserVal.(*user.User) + if !ok { + handler.Logger.Error("cannot cast user in request context while load order") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + withdrawSum, err := handler.WithdrawController.GetWithdrawSumByUser(request.Context(), ctxUser) + if err != nil { + handler.Logger.Error("error while get withdraw sum for user", zap.Int("user id", ctxUser.ID), zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + res := GetWithdrawResponse{ + Current: float32(ctxUser.Balance) / 100, + Withdrawn: float32(withdrawSum) / 100, + } + + body, err := json.Marshal(res) + if err != nil { + handler.Logger.Error("cannot marshal info of user balance", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + _, err = writer.Write(body) + if err != nil { + handler.Logger.Error("error while write ifo of user balance to body") + writer.WriteHeader(http.StatusInternalServerError) + } +} + +func (handler *BalanceHandler) HandleAddWithdraw(writer http.ResponseWriter, request *http.Request) { + ctxUserVal := request.Context().Value(AuthorizeUserContext) + if ctxUserVal == nil { + handler.Logger.Error("load orders handler must have user in context, but not found") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + ctxUser, ok := ctxUserVal.(*user.User) + if !ok { + handler.Logger.Error("cannot cast user in request context while load order") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + bf := bytes.Buffer{} + _, err := bf.ReadFrom(request.Body) + if err != nil { + handler.Logger.Debug("user sends invalid body to withdraw", zap.Error(err)) + writer.WriteHeader(http.StatusBadRequest) + return + } + + var requestWithdraw AddWithdrawRequest + err = json.Unmarshal(bf.Bytes(), &requestWithdraw) + if err != nil || requestWithdraw.Sum == "" || requestWithdraw.Order == "" { + handler.Logger.Debug("user sends invalid withdraw request object", zap.Error(err)) + writer.WriteHeader(http.StatusBadRequest) + return + } + + sum, err := user.ParseBalance(string(requestWithdraw.Sum)) + if err != nil { + handler.Logger.Debug("user sends invalid sum of withdraw", zap.Error(err)) + writer.WriteHeader(http.StatusUnprocessableEntity) + return + } + + withdraw, err := handler.WithdrawController.CreateWithdrawByUser(request.Context(), ctxUser, requestWithdraw.Order, sum) + if err != nil { + if errors.Is(err, order.ErrInvalidOrderNumber) { + writer.WriteHeader(http.StatusUnprocessableEntity) + handler.Logger.Debug("user send invalid format of order number", zap.String("order number", requestWithdraw.Order)) + } else if errors.Is(err, order.ErrNoMoney) { + writer.WriteHeader(http.StatusPaymentRequired) + handler.Logger.Debug("user has no money to withdraw", zap.Int64("user balance", ctxUser.Balance), zap.Int64("required sum", sum)) + } else { + writer.WriteHeader(http.StatusInternalServerError) + handler.Logger.Error("server error while create withdraw", zap.Error(err)) + } + + return + } + + handler.Logger.Debug("user successful register withdraw", zap.String("order ID", withdraw.OrderID), zap.Int64("sum", withdraw.Sum)) + writer.WriteHeader(http.StatusOK) } -func (handler *BalanceHandler) HandleWithdraw(writer http.ResponseWriter, request *http.Request) { - writer.Write([]byte("Withdraw....")) +func (handler *BalanceHandler) HandleGetListWithdraw(writer http.ResponseWriter, request *http.Request) { + ctxUserVal := request.Context().Value(AuthorizeUserContext) + if ctxUserVal == nil { + handler.Logger.Error("load orders handler must have user in context, but not found") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + ctxUser, ok := ctxUserVal.(*user.User) + if !ok { + handler.Logger.Error("cannot cast user in request context while load order") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + withdrawList, err := handler.WithdrawController.GetWithdrawByUser(request.Context(), ctxUser) + if err != nil { + handler.Logger.Error("error while get list of withdraw", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + if len(withdrawList) == 0 { + handler.Logger.Debug("user get list of withdraw but he have no one withdraw", zap.Int("user id", ctxUser.ID)) + writer.WriteHeader(http.StatusNoContent) + return + } + + responseBody := make([]GetWithdrawItemResponse, len(withdrawList)) + for i := 0; i < len(withdrawList); i++ { + responseBody[i] = GetWithdrawItemResponse{ + Order: withdrawList[i].OrderID, + Sum: float32(withdrawList[i].Sum) / 100, + ProcessedAt: withdrawList[i].ProcessedAt, + } + } + + rs, err := json.Marshal(responseBody) + if err != nil { + handler.Logger.Error("error while marshal list of withdraw", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + return + } + + _, err = writer.Write(rs) + if err != nil { + handler.Logger.Error("error while write list of withdraw", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + } } diff --git a/internal/order/enrollment.go b/internal/order/enrollment.go index be77d5c..77ad3d4 100644 --- a/internal/order/enrollment.go +++ b/internal/order/enrollment.go @@ -40,6 +40,7 @@ type EnrollmentController struct { orderServiceAddr string repository EnrollmentRepository userController *user.Controller + dataSource DataSource } type Enrollment struct { @@ -59,7 +60,7 @@ type EnrollmentRepository interface { } func NewEnrollmentController(orderServiceAddr string, repository EnrollmentRepository, userController *user.Controller) *EnrollmentController { - return &EnrollmentController{orderServiceAddr: orderServiceAddr, repository: repository, userController: userController} // TODO: Create channel handler + return &EnrollmentController{orderServiceAddr: orderServiceAddr, repository: repository, userController: userController} } func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderNumber string, userID int) (*Enrollment, error) { @@ -125,6 +126,8 @@ func (controller *EnrollmentController) LoadOrder(ctx context.Context, ownerID i continue } + // need transaction + enrollmentRepository.ChangeStatus(context.TODO(), orderNumber, EnrollmentStatusProcessed) owner, err := userController.GetUserByID(context.TODO(), ownerID) if err != nil { diff --git a/internal/order/order.go b/internal/order/order.go index dcfba6f..c237c4a 100644 --- a/internal/order/order.go +++ b/internal/order/order.go @@ -7,6 +7,15 @@ import ( var ErrInvalidOrderNumber = errors.New("invalid order number") +type DataSource interface { + Begin() (*Transaction, error) +} + +type Transaction interface { + Commit() error + Rollback() error +} + func IsOrderNumberCorrect(orderNumber string) bool { sum := 0 diff --git a/internal/order/pgx-withdraw-repository.go b/internal/order/pgx-withdraw-repository.go new file mode 100644 index 0000000..f826b8b --- /dev/null +++ b/internal/order/pgx-withdraw-repository.go @@ -0,0 +1,74 @@ +package order + +import ( + "context" + "database/sql" + "fmt" + "time" +) + +type PGXWithdrawRepository struct { + db *sql.DB +} + +func NewPGXWithdrawRepository(db *sql.DB) *PGXWithdrawRepository { + return &PGXWithdrawRepository{db: db} +} + +func (repository *PGXWithdrawRepository) AddWithdraw(ctx context.Context, userID int, orderID string, sum int64) (*Withdraw, error) { + now := time.Now() + _, err := repository.db.ExecContext(ctx, "INSERT INTO withdraw_order (order_id, user_id, sum, processed_at) VALUES ($1, $2, $3, $4)", orderID, userID, sum, now) + + if err != nil { + return nil, fmt.Errorf("error while creating user withdraw: %w", err) + } + + withdraw := Withdraw{ + OrderID: orderID, + Sum: sum, + ProcessedAt: now, + } + + return &withdraw, nil +} + +func (repository *PGXWithdrawRepository) GetWithdrawListByUserID(ctx context.Context, userID int) ([]*Withdraw, error) { + rows, err := repository.db.QueryContext(ctx, "SELECT sum, order_id, processed_at FROM withdraw_order WHERE user_id = $1 ORDER BY processed_at ASC", userID) + if err != nil { + return nil, err + } + defer rows.Close() + + withdrawList := make([]*Withdraw, 0) + + for rows.Next() { + if err = rows.Err(); err != nil { + return nil, err + } + + withdraw := Withdraw{} + err = rows.Scan(&withdraw.Sum, &withdraw.OrderID, &withdraw.ProcessedAt) + if err != nil { + return nil, err + } + + withdrawList = append(withdrawList, &withdraw) + } + + return withdrawList, nil +} + +func (repository *PGXWithdrawRepository) GetWithdrawSumByUserID(ctx context.Context, userID int) (int64, error) { + row := repository.db.QueryRowContext(ctx, "SELECT COALESCE(SUM(sum),0) FROM withdraw_order WHERE user_id = $1", userID) + + if row.Err() != nil { + return 0, fmt.Errorf("error while query for get user withdraw sum by id: %w", row.Err()) + } + + var sum int64 + if err := row.Scan(&sum); err != nil { + return 0, fmt.Errorf("error while scan sum after query of user withdraw sum: %w", err) + } + + return sum, nil +} diff --git a/internal/order/withdraw.go b/internal/order/withdraw.go new file mode 100644 index 0000000..4bc5f4e --- /dev/null +++ b/internal/order/withdraw.go @@ -0,0 +1,72 @@ +package order + +import ( + "context" + "errors" + "github.com/nessai1/gophermat/internal/user" + "time" +) + +var ErrNoMoney = errors.New("user has no money to transit") + +type Withdraw struct { + Sum int64 + OrderID string + ProcessedAt time.Time +} + +type WithdrawRepository interface { + AddWithdraw(ctx context.Context, userID int, orderID string, sum int64) (*Withdraw, error) + GetWithdrawListByUserID(ctx context.Context, userID int) ([]*Withdraw, error) + GetWithdrawSumByUserID(ctx context.Context, userID int) (int64, error) +} + +type WithdrawController struct { + repository WithdrawRepository + userController *user.Controller +} + +func NewWithdrawController(repository WithdrawRepository, userController *user.Controller) *WithdrawController { + return &WithdrawController{ + repository: repository, + userController: userController, + } +} + +func (controller *WithdrawController) CreateWithdrawByUser(ctx context.Context, innerUser *user.User, orderID string, sum int64) (*Withdraw, error) { + // need transaction + + if !IsOrderNumberCorrect(orderID) { + return nil, ErrInvalidOrderNumber + } + + if sum > innerUser.Balance { + return nil, ErrNoMoney + } + + balance := innerUser.Balance - sum + err := controller.userController.SetUserBalanceByID(ctx, innerUser.ID, balance) + if err != nil { + return nil, err + } + + withdraw, err := controller.repository.AddWithdraw(ctx, innerUser.ID, orderID, sum) + + if err != nil { + return nil, err + } + + // stop transaction + + return withdraw, nil +} + +func (controller *WithdrawController) GetWithdrawByUser(ctx context.Context, innerUser *user.User) ([]*Withdraw, error) { + list, err := controller.repository.GetWithdrawListByUserID(ctx, innerUser.ID) + return list, err +} + +func (controller *WithdrawController) GetWithdrawSumByUser(ctx context.Context, innerUser *user.User) (int64, error) { + sum, err := controller.repository.GetWithdrawSumByUserID(ctx, innerUser.ID) + return sum, err +} diff --git a/migrations/002_create_enrollment_order_table.up.sql b/migrations/002_create_enrollment_order_table.up.sql index 4923332..4bc2dc7 100644 --- a/migrations/002_create_enrollment_order_table.up.sql +++ b/migrations/002_create_enrollment_order_table.up.sql @@ -1,6 +1,6 @@ BEGIN; CREATE TABLE enrollment_order ( - order_id varchar(16) not null PRIMARY KEY, + order_id varchar(100) not null PRIMARY KEY, user_id int not null references "user" (id), status varchar(30) not null, accrual bigint not null default 0, diff --git a/migrations/003_create_withdraw_order_table.down.sql b/migrations/003_create_withdraw_order_table.down.sql new file mode 100644 index 0000000..36437fa --- /dev/null +++ b/migrations/003_create_withdraw_order_table.down.sql @@ -0,0 +1,4 @@ +BEGIN; +DROP INDEX IF EXISTS withdraw_order_user_id_idx; +DROP TABLE IF EXISTS withdraw_order; +COMMIT; \ No newline at end of file diff --git a/migrations/003_create_withdraw_order_table.up.sql b/migrations/003_create_withdraw_order_table.up.sql new file mode 100644 index 0000000..169418b --- /dev/null +++ b/migrations/003_create_withdraw_order_table.up.sql @@ -0,0 +1,10 @@ +BEGIN; +CREATE TABLE IF NOT EXISTS withdraw_order ( + id serial PRIMARY KEY, + order_id varchar(100) not null, + user_id int not null references "user" (id), + processed_at timestamp not null default now(), + sum bigint not null +); +CREATE INDEX withdraw_order_user_id_idx ON withdraw_order (user_id); +COMMIT; \ No newline at end of file From 88098bfc0afc8f226b112ae8716d3f58adfc9d9e Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 4 Dec 2023 02:25:59 +0200 Subject: [PATCH 24/29] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20=D0=B2=D0=BE=D1=80=D0=BA=D0=B5=D1=80?= =?UTF-8?q?=D0=BE=D0=B2;=20=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81?= =?UTF-8?q?=20=D1=82=D1=80=D0=B0=D0=BD=D0=B7=D0=B0=D0=BA=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gophermart/gophermart.go | 34 ++-- internal/handler/balance-handler.go | 3 + internal/intransaction/intransaction.go | 44 +++++ internal/order/enrollment-worker.go | 178 ++++++++++++++++++++ internal/order/enrollment.go | 79 ++------- internal/order/order.go | 10 +- internal/order/pgx-enrollment-repository.go | 26 +++ internal/order/withdraw.go | 36 ++-- internal/user/user.go | 4 + 9 files changed, 319 insertions(+), 95 deletions(-) create mode 100644 internal/intransaction/intransaction.go create mode 100644 internal/order/enrollment-worker.go diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index ef75e17..01594f5 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -2,16 +2,16 @@ package gophermart import ( "fmt" + "github.com/go-chi/chi" "github.com/nessai1/gophermat/internal/config" "github.com/nessai1/gophermat/internal/database" "github.com/nessai1/gophermat/internal/handler" + "github.com/nessai1/gophermat/internal/intransaction" "github.com/nessai1/gophermat/internal/logger" "github.com/nessai1/gophermat/internal/order" "github.com/nessai1/gophermat/internal/user" - "net/http" - - "github.com/go-chi/chi" "go.uber.org/zap" + "net/http" ) func Start() error { @@ -38,28 +38,38 @@ func Start() error { authMux.Post("/api/user/register", authHandler.HandleRegisterUser) authMux.Post("/api/user/login", authHandler.HandleAuthUser) - enrollmentController := handler.EnrollmentOrderHandler{ + transaction := intransaction.NewPGXTransaction(db) + + enrollmentController := order.NewEnrollmentController(cfg.AccrualServiceAddr, order.CreatePGXEnrollmentRepository(db), userController) + ch, err := order.StartEnrollmentWorker(userController, enrollmentController, log, cfg.AccrualServiceAddr, transaction) + if err != nil { + return fmt.Errorf("error while starting enrollment worker: %w", err) + } + + enrollmentController.EnrollmentCh = ch + + enrollmentHandler := handler.EnrollmentOrderHandler{ Logger: log, - EnrollmentController: order.NewEnrollmentController(cfg.AccrualServiceAddr, order.CreatePGXEnrollmentRepository(db), userController), + EnrollmentController: enrollmentController, } enrollmentMux := chi.NewMux() enrollmentMux.Use(authHandler.MiddlewareAuthorizeRequest()) - enrollmentMux.Post("/", enrollmentController.HandleLoadOrders) - enrollmentMux.Get("/", enrollmentController.HandleGetOrders) + enrollmentMux.Post("/", enrollmentHandler.HandleLoadOrders) + enrollmentMux.Get("/", enrollmentHandler.HandleGetOrders) - balanceController := handler.BalanceHandler{ + balanceHandler := handler.BalanceHandler{ Logger: log, - WithdrawController: order.NewWithdrawController(order.NewPGXWithdrawRepository(db), userController), + WithdrawController: order.NewWithdrawController(order.NewPGXWithdrawRepository(db), userController, transaction), } balanceMux := chi.NewMux() balanceMux.Use(authHandler.MiddlewareAuthorizeRequest()) - balanceMux.Get("/", balanceController.HandleGetBalance) - balanceMux.Post("/withdraw", balanceController.HandleAddWithdraw) + balanceMux.Get("/", balanceHandler.HandleGetBalance) + balanceMux.Post("/withdraw", balanceHandler.HandleAddWithdraw) withdrawInfoMux := chi.NewMux() withdrawInfoMux.Use(authHandler.MiddlewareAuthorizeRequest()) - withdrawInfoMux.Get("/", balanceController.HandleGetListWithdraw) + withdrawInfoMux.Get("/", balanceHandler.HandleGetListWithdraw) router.Mount("/", authMux) router.Mount("/api/user/orders", enrollmentMux) diff --git a/internal/handler/balance-handler.go b/internal/handler/balance-handler.go index dc62c90..da4d412 100644 --- a/internal/handler/balance-handler.go +++ b/internal/handler/balance-handler.go @@ -119,6 +119,9 @@ func (handler *BalanceHandler) HandleAddWithdraw(writer http.ResponseWriter, req } else if errors.Is(err, order.ErrNoMoney) { writer.WriteHeader(http.StatusPaymentRequired) handler.Logger.Debug("user has no money to withdraw", zap.Int64("user balance", ctxUser.Balance), zap.Int64("required sum", sum)) + } else if errors.Is(err, order.ErrEmptyBalance) { + writer.WriteHeader(http.StatusBadRequest) + handler.Logger.Debug("user sends empty sum to withdraw") } else { writer.WriteHeader(http.StatusInternalServerError) handler.Logger.Error("server error while create withdraw", zap.Error(err)) diff --git a/internal/intransaction/intransaction.go b/internal/intransaction/intransaction.go new file mode 100644 index 0000000..25cfb5a --- /dev/null +++ b/internal/intransaction/intransaction.go @@ -0,0 +1,44 @@ +package intransaction + +import ( + "context" + "database/sql" + "errors" + "fmt" +) + +type Transaction interface { + InTransaction(context.Context, func(innerCtx context.Context) error) error +} + +type PGXTransaction struct { + db *sql.DB +} + +func NewPGXTransaction(db *sql.DB) Transaction { + return &PGXTransaction{db: db} +} + +func (transaction *PGXTransaction) InTransaction(ctx context.Context, consistent func(innerCtx context.Context) error) error { + tx, err := transaction.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("error while start transaction: %w", err) + } + + err = consistent(ctx) + if err != nil { + rollErr := tx.Rollback() + if rollErr != nil { + return fmt.Errorf("error while rollback unsuccessful transaction: %w", errors.Join(err, rollErr)) + } + + return fmt.Errorf("rollback unsuccessful transaction: %w", err) + } + + commitErr := tx.Commit() + if commitErr != nil { + return fmt.Errorf("error while commit successful transaction: %w", commitErr) + } + + return nil +} diff --git a/internal/order/enrollment-worker.go b/internal/order/enrollment-worker.go new file mode 100644 index 0000000..4780a96 --- /dev/null +++ b/internal/order/enrollment-worker.go @@ -0,0 +1,178 @@ +package order + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/nessai1/gophermat/internal/intransaction" + "github.com/nessai1/gophermat/internal/user" + "go.uber.org/zap" + "net/http" + "strconv" + "time" +) + +type EnrollmentWorker struct { + ch chan Enrollment + client http.Client + enrollmentController *EnrollmentController + userController *user.Controller + logger *zap.Logger + serviceAddr string + transaction intransaction.Transaction +} + +func (worker *EnrollmentWorker) Listen() { + for enrollment := range worker.ch { + worker.requireOrder(&enrollment) + } +} + +func (worker *EnrollmentWorker) requireOrder(enrollment *Enrollment) { + worker.logger.Debug("start required order", zap.String("order id", enrollment.OrderID), zap.String("order init status", enrollment.Status)) + err := worker.enrollmentController.ChangeStatusByOrderID(context.TODO(), enrollment.OrderID, EnrollmentStatusProcessing) + if err != nil { + worker.logger.Error("error while try to change order status on processing in worker", zap.Error(err), zap.String("order id", enrollment.OrderID)) + go func(e Enrollment) { + time.Sleep(time.Second * 5) // задержка что-бы не заспамить в случае какой-то поломки СУБД + worker.ch <- e // кладем в конец очереди на следующую попытку + }(*enrollment) + } + + for { + ctx := context.TODO() + resp, err := worker.client.Get(worker.serviceAddr + "/api/orders/" + enrollment.OrderID) + if err != nil { + break + } + + if resp.StatusCode == http.StatusTooManyRequests { + retryAfter := resp.Header.Get("Retry-After") + retryAfterInt, _ := strconv.Atoi(retryAfter) + worker.logger.Info("got many request to accrual service", zap.Int("retry after", retryAfterInt)) + time.Sleep(time.Second * time.Duration(retryAfterInt)) + continue + } + + if resp.StatusCode != http.StatusOK { + worker.logger.Error("got unsuccessful status from accrual service", zap.String("status", resp.Status)) + time.Sleep(time.Second * 5) + continue + } + + var buffer bytes.Buffer + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + worker.logger.Error("error while read response from accrual service", zap.String("order id", enrollment.OrderID)) + time.Sleep(time.Second * 5) + continue + } + + var accrualInfo OrderAccrualInfo + err = json.Unmarshal(buffer.Bytes(), &accrualInfo) + if err != nil { + worker.logger.Error("error while unmarshal response from accrual service", zap.Error(err)) + } + + if accrualInfo.Status == orderAccrualStatusInvalid { + err = worker.enrollmentController.ChangeStatusByOrderID(ctx, enrollment.OrderID, EnrollmentStatusInvalid) + if err != nil { + worker.logger.Error("error while change status in accrual worker", zap.Error(err), zap.String("status", EnrollmentStatusInvalid), zap.String("order id", enrollment.OrderID)) + time.Sleep(time.Second * 5) + continue + } + + worker.logger.Info("accrual complete", zap.String("status", EnrollmentStatusInvalid), zap.String("order id", enrollment.OrderID)) + return + } + + if accrualInfo.Status == orderAccrualStatusProcessing || accrualInfo.Status == orderAccrualStatusRegistered { + worker.logger.Info("order is still awaiting accrual", zap.String("current status", accrualInfo.Status), zap.String("order id", enrollment.OrderID)) + time.Sleep(time.Second * 5) + continue + } + + err = worker.transaction.InTransaction(ctx, func(innerCtx context.Context) error { + + txErr := worker.enrollmentController.ChangeStatusByOrderID(innerCtx, accrualInfo.Order, EnrollmentStatusProcessed) + if txErr != nil { + return fmt.Errorf("error while change order status: %w", txErr) + } + + df, txErr := user.ParseBalance(string(accrualInfo.Accrual)) + if txErr != nil { + return fmt.Errorf("error while parse balance: %w", txErr) + } + + txErr = worker.enrollmentController.UpdateOrderAccrualByID(innerCtx, enrollment.OrderID, int(df)) + if txErr != nil { + return fmt.Errorf("error while get user from controller: %w", txErr) + } + + owner, txErr := worker.userController.GetUserByID(innerCtx, enrollment.UserID) + if err != nil { + return fmt.Errorf("error while get user: %w", txErr) + } + + balance := owner.Balance + df + txErr = worker.userController.SetUserBalanceByID(context.TODO(), owner.ID, balance) + if txErr != nil { + return fmt.Errorf("error while update user balance: %w", txErr) + } + + worker.logger.Error("successful accruled order", zap.String("order id", enrollment.OrderID), zap.Int("sum", int(df))) + + return nil + }) + + if err != nil { + worker.logger.Error("error while make user-accrual update transaction", zap.Error(err)) + time.Sleep(time.Second * 5) + continue + } + + return + } +} + +func StartEnrollmentWorker(userController *user.Controller, enrollmentController *EnrollmentController, logger *zap.Logger, serviceAddr string, transaction intransaction.Transaction) (chan<- Enrollment, error) { + ch := make(chan Enrollment, 10) + + enrollmentList, err := enrollmentController.GetProcessedEnrollments(context.TODO()) + if err != nil { + return nil, fmt.Errorf("error while get new enrollments: %w", err) + } + + if len(enrollmentList) > 0 { + logger.Info("service has unresolved orders, start preload goroutine", zap.Int("enrollments len", len(enrollmentList))) + go preloadOrders(enrollmentList, ch) + } + + go func(ch chan Enrollment) { + client := http.Client{ + Timeout: time.Second * 10, + } + + worker := EnrollmentWorker{ + + client: client, + ch: ch, + enrollmentController: enrollmentController, + userController: userController, + serviceAddr: serviceAddr, + logger: logger, + transaction: transaction, + } + + worker.Listen() + }(ch) + + return ch, nil +} + +func preloadOrders(enrollments []*Enrollment, ch chan<- Enrollment) { + for _, enrollment := range enrollments { + ch <- *enrollment + } +} diff --git a/internal/order/enrollment.go b/internal/order/enrollment.go index 77ad3d4..8de90b2 100644 --- a/internal/order/enrollment.go +++ b/internal/order/enrollment.go @@ -1,14 +1,11 @@ package order import ( - "bytes" "context" "encoding/json" "errors" "fmt" "github.com/nessai1/gophermat/internal/user" - "net/http" - "strconv" "time" ) @@ -40,7 +37,7 @@ type EnrollmentController struct { orderServiceAddr string repository EnrollmentRepository userController *user.Controller - dataSource DataSource + EnrollmentCh chan<- Enrollment } type Enrollment struct { @@ -57,6 +54,7 @@ type EnrollmentRepository interface { ChangeStatus(ctx context.Context, orderID, status string) error UpdateOrderAccrual(ctx context.Context, orderID string, accrual int) error GetListByUserID(ctx context.Context, userID int) ([]*Enrollment, error) + GetProcessedEnrollments(ctx context.Context) ([]*Enrollment, error) } func NewEnrollmentController(orderServiceAddr string, repository EnrollmentRepository, userController *user.Controller) *EnrollmentController { @@ -79,7 +77,7 @@ func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderN } if enrollment.UserID == userID && enrollment.Status == EnrollmentStatusNew { - err = controller.LoadOrder(ctx, userID, enrollment.OrderID) + go controller.loadOrder(enrollment) if err != nil { return nil, fmt.Errorf("error while start order loading operation: %w", err) } @@ -88,67 +86,18 @@ func (controller *EnrollmentController) RequireOrder(ctx context.Context, orderN return enrollment, nil } -func (controller *EnrollmentController) LoadOrder(ctx context.Context, ownerID int, orderNumber string) error { - err := controller.repository.ChangeStatus(ctx, orderNumber, EnrollmentStatusProcessing) - if err != nil { - return fmt.Errorf("error while update order status in require: %w", err) - } +func (controller *EnrollmentController) ChangeStatusByOrderID(ctx context.Context, orderID, status string) error { + return controller.repository.ChangeStatus(ctx, orderID, status) +} - go func(serviceAddr, orderNumber string, ownerID int, enrollmentRepository EnrollmentRepository, userController *user.Controller) { - for { - resp, err := http.Get(serviceAddr + "/api/orders/" + orderNumber) - if err != nil { - break - } - - if resp.StatusCode == http.StatusTooManyRequests { - retryAfter := resp.Header.Get("Retry-After") - retryAfterInt, _ := strconv.Atoi(retryAfter) - time.Sleep(time.Second * time.Duration(retryAfterInt)) - continue - } - - if resp.StatusCode != http.StatusOK { - break - } - - var buffer bytes.Buffer - buffer.ReadFrom(resp.Body) - var accrualInfo OrderAccrualInfo - json.Unmarshal(buffer.Bytes(), &accrualInfo) - if accrualInfo.Status == orderAccrualStatusInvalid { - enrollmentRepository.ChangeStatus(context.TODO(), orderNumber, EnrollmentStatusInvalid) - return - } - - if accrualInfo.Status == orderAccrualStatusProcessing || accrualInfo.Status == orderAccrualStatusRegistered { - time.Sleep(time.Second * 5) - continue - } - - // need transaction - - enrollmentRepository.ChangeStatus(context.TODO(), orderNumber, EnrollmentStatusProcessed) - owner, err := userController.GetUserByID(context.TODO(), ownerID) - if err != nil { - return - } - - df, err := user.ParseBalance(string(accrualInfo.Accrual)) - if err != nil { - return - } - - enrollmentRepository.UpdateOrderAccrual(context.TODO(), orderNumber, int(df)) - - balance := owner.Balance + df - userController.SetUserBalanceByID(context.TODO(), ownerID, balance) - return - } +func (controller *EnrollmentController) GetProcessedEnrollments(ctx context.Context) ([]*Enrollment, error) { + enrollmentList, err := controller.repository.GetProcessedEnrollments(ctx) - }(controller.orderServiceAddr, orderNumber, ownerID, controller.repository, controller.userController) + return enrollmentList, err +} - return nil +func (controller *EnrollmentController) loadOrder(enrollment *Enrollment) { + controller.EnrollmentCh <- *enrollment } func (controller *EnrollmentController) GetUserOrderListByID(ctx context.Context, userID int) ([]*Enrollment, error) { @@ -156,3 +105,7 @@ func (controller *EnrollmentController) GetUserOrderListByID(ctx context.Context return enrollmentList, err } + +func (controller *EnrollmentController) UpdateOrderAccrualByID(ctx context.Context, orderID string, accrual int) error { + return controller.repository.UpdateOrderAccrual(ctx, orderID, accrual) +} diff --git a/internal/order/order.go b/internal/order/order.go index c237c4a..c5f7871 100644 --- a/internal/order/order.go +++ b/internal/order/order.go @@ -6,15 +6,7 @@ import ( ) var ErrInvalidOrderNumber = errors.New("invalid order number") - -type DataSource interface { - Begin() (*Transaction, error) -} - -type Transaction interface { - Commit() error - Rollback() error -} +var ErrEmptyBalance = errors.New("empty balance") func IsOrderNumberCorrect(orderNumber string) bool { sum := 0 diff --git a/internal/order/pgx-enrollment-repository.go b/internal/order/pgx-enrollment-repository.go index b9e4f94..e02bba4 100644 --- a/internal/order/pgx-enrollment-repository.go +++ b/internal/order/pgx-enrollment-repository.go @@ -95,3 +95,29 @@ func (repository *PGXEnrollmentRepository) GetListByUserID(ctx context.Context, return enrollmentList, nil } + +func (repository *PGXEnrollmentRepository) GetProcessedEnrollments(ctx context.Context) ([]*Enrollment, error) { + rows, err := repository.db.QueryContext(ctx, "SELECT order_id, user_id, status, accrual, uploaded_at FROM enrollment_order WHERE status IN ($1, $2) ORDER BY uploaded_at ASC", EnrollmentStatusNew, EnrollmentStatusProcessing) + if err != nil { + return nil, err + } + defer rows.Close() + + enrollmentList := make([]*Enrollment, 0) + + for rows.Next() { + if err = rows.Err(); err != nil { + return nil, err + } + + enrollment := Enrollment{} + err = rows.Scan(&enrollment.OrderID, &enrollment.UserID, &enrollment.Status, &enrollment.Accrual, &enrollment.UploadedAt) + if err != nil { + return nil, err + } + + enrollmentList = append(enrollmentList, &enrollment) + } + + return enrollmentList, nil +} diff --git a/internal/order/withdraw.go b/internal/order/withdraw.go index 4bc5f4e..5e154bb 100644 --- a/internal/order/withdraw.go +++ b/internal/order/withdraw.go @@ -3,6 +3,8 @@ package order import ( "context" "errors" + "fmt" + "github.com/nessai1/gophermat/internal/intransaction" "github.com/nessai1/gophermat/internal/user" "time" ) @@ -24,40 +26,52 @@ type WithdrawRepository interface { type WithdrawController struct { repository WithdrawRepository userController *user.Controller + transaction intransaction.Transaction } -func NewWithdrawController(repository WithdrawRepository, userController *user.Controller) *WithdrawController { +func NewWithdrawController(repository WithdrawRepository, userController *user.Controller, transaction intransaction.Transaction) *WithdrawController { return &WithdrawController{ repository: repository, userController: userController, + transaction: transaction, } } func (controller *WithdrawController) CreateWithdrawByUser(ctx context.Context, innerUser *user.User, orderID string, sum int64) (*Withdraw, error) { - // need transaction if !IsOrderNumberCorrect(orderID) { return nil, ErrInvalidOrderNumber } + if sum == 0 { + return nil, ErrEmptyBalance + } + if sum > innerUser.Balance { return nil, ErrNoMoney } - balance := innerUser.Balance - sum - err := controller.userController.SetUserBalanceByID(ctx, innerUser.ID, balance) - if err != nil { - return nil, err - } + var withdraw *Withdraw + err := controller.transaction.InTransaction(ctx, func(innerCtx context.Context) error { + balance := innerUser.Balance - sum + txErr := controller.userController.SetUserBalanceByID(ctx, innerUser.ID, balance) + if txErr != nil { + return fmt.Errorf("error while set user balance: %w", txErr) + } - withdraw, err := controller.repository.AddWithdraw(ctx, innerUser.ID, orderID, sum) + withdraw, txErr = controller.repository.AddWithdraw(ctx, innerUser.ID, orderID, sum) + + if txErr != nil { + return fmt.Errorf("error while add withdraw: %w", txErr) + } + + return nil + }) if err != nil { - return nil, err + return nil, fmt.Errorf("error while create withdraw by user: %w", err) } - // stop transaction - return withdraw, nil } diff --git a/internal/user/user.go b/internal/user/user.go index 0ee9b92..f918660 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -22,6 +22,10 @@ type User struct { } func ParseBalance(balance string) (int64, error) { + if balance == "" { + return 0, nil + } + parts := strings.Split(balance, ".") if len(parts) > 2 { From 22989899044df161d3be38fc5cf94a5397ad1ffb Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 4 Dec 2023 02:33:40 +0200 Subject: [PATCH 25/29] Add gzip middleware --- internal/gophermart/gophermart.go | 3 ++ internal/zip/zip.go | 56 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 internal/zip/zip.go diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index 01594f5..567b0c5 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -10,6 +10,7 @@ import ( "github.com/nessai1/gophermat/internal/logger" "github.com/nessai1/gophermat/internal/order" "github.com/nessai1/gophermat/internal/user" + "github.com/nessai1/gophermat/internal/zip" "go.uber.org/zap" "net/http" ) @@ -34,6 +35,8 @@ func Start() error { UserController: userController, } + router.Use(zip.GetZipMiddleware(log)) + authMux := chi.NewMux() authMux.Post("/api/user/register", authHandler.HandleRegisterUser) authMux.Post("/api/user/login", authHandler.HandleAuthUser) diff --git a/internal/zip/zip.go b/internal/zip/zip.go new file mode 100644 index 0000000..15a77bd --- /dev/null +++ b/internal/zip/zip.go @@ -0,0 +1,56 @@ +package zip + +import ( + "compress/gzip" + "io" + "net/http" + "strings" + + "go.uber.org/zap" +) + +type gzipWriter struct { + http.ResponseWriter + Writer io.Writer +} + +func (writer gzipWriter) Write(bytes []byte) (int, error) { + return writer.Writer.Write(bytes) +} + +func GetZipMiddleware(logger *zap.Logger) func(handler http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if !strings.Contains(request.Header.Get("Accept-Encoding"), "gzip") { + logger.Debug("Client doesn't accept gzip format.") + next.ServeHTTP(writer, request) + return + } + + gz, err := gzip.NewWriterLevel(writer, gzip.BestSpeed) + if err != nil { + logger.Fatal("Gzip encoding level doesn't work!", zap.Error(err)) + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte("Internal error while encode content to gzip: " + err.Error())) + return + } + + defer gz.Close() + + writer = gzipWriter{ + ResponseWriter: writer, + Writer: gz, + } + writer.Header().Set("Content-Encoding", "gzip") + + if strings.Contains(request.Header.Get("Content-Encoding"), "gzip") { + request.Body, err = gzip.NewReader(request.Body) + if err != nil { + logger.Fatal("Internal error while encode body content to gzip", zap.Error(err)) + } + } + + next.ServeHTTP(writer, request) + }) + } +} From 96fe85c9101423b5c3ee4fa87560033093fc0a94 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 4 Dec 2023 02:50:41 +0200 Subject: [PATCH 26/29] Add content type to handlers --- internal/handler/balance-handler.go | 2 ++ internal/handler/enrollment-order-handler.go | 1 + internal/handler/order-handler.go | 10 ---------- 3 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 internal/handler/order-handler.go diff --git a/internal/handler/balance-handler.go b/internal/handler/balance-handler.go index da4d412..a51b690 100644 --- a/internal/handler/balance-handler.go +++ b/internal/handler/balance-handler.go @@ -66,6 +66,7 @@ func (handler *BalanceHandler) HandleGetBalance(writer http.ResponseWriter, requ return } + writer.Header().Set("Content-Type", "application/json") _, err = writer.Write(body) if err != nil { handler.Logger.Error("error while write ifo of user balance to body") @@ -178,6 +179,7 @@ func (handler *BalanceHandler) HandleGetListWithdraw(writer http.ResponseWriter, return } + writer.Header().Set("Content-Type", "application/json") _, err = writer.Write(rs) if err != nil { handler.Logger.Error("error while write list of withdraw", zap.Error(err)) diff --git a/internal/handler/enrollment-order-handler.go b/internal/handler/enrollment-order-handler.go index 65d1d35..8ee5f87 100644 --- a/internal/handler/enrollment-order-handler.go +++ b/internal/handler/enrollment-order-handler.go @@ -130,6 +130,7 @@ func (handler *EnrollmentOrderHandler) HandleGetOrders(writer http.ResponseWrite return } + writer.Header().Set("Content-Type", "application/json") _, err = writer.Write(rs) if err != nil { handler.Logger.Error("error while write enrollment list to result body", zap.Error(err)) diff --git a/internal/handler/order-handler.go b/internal/handler/order-handler.go deleted file mode 100644 index bf785cd..0000000 --- a/internal/handler/order-handler.go +++ /dev/null @@ -1,10 +0,0 @@ -package handler - -import "net/http" - -type OrderHandler struct { -} - -func (handler *OrderHandler) HandleGetUserOrders(writer http.ResponseWriter, request *http.Request) { - -} From 75ac0866ed0c58a12f41e1b9f3c157e7648c6f1c Mon Sep 17 00:00:00 2001 From: nessai1 Date: Mon, 4 Dec 2023 03:00:27 +0200 Subject: [PATCH 27/29] Fix vet testes --- internal/handler/auth-handler.go | 6 ------ internal/logger/zap-logger.go | 2 +- internal/order/enrollment-worker.go | 3 +++ internal/user/map-repository.go | 3 ++- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/handler/auth-handler.go b/internal/handler/auth-handler.go index c9960b5..efa5b7b 100644 --- a/internal/handler/auth-handler.go +++ b/internal/handler/auth-handler.go @@ -83,10 +83,7 @@ func (handler *AuthHandler) HandleAuthUser(writer http.ResponseWriter, request * c := &http.Cookie{Name: authCookieName, Value: sign} http.SetCookie(writer, c) - ctx := request.Context() - handler.Logger.Debug("user successful authorized by request", zap.String("user login", fetchedUser.Login)) - request = request.WithContext(context.WithValue(ctx, AuthorizeUserContext, fetchedUser)) writer.WriteHeader(http.StatusOK) } @@ -138,9 +135,6 @@ func (handler *AuthHandler) HandleRegisterUser(writer http.ResponseWriter, reque handler.Logger.Debug("user successful registered by request", zap.String("user login", createdUser.Login)) - ctx := request.Context() - request = request.WithContext(context.WithValue(ctx, AuthorizeUserContext, createdUser)) - writer.WriteHeader(http.StatusOK) } diff --git a/internal/logger/zap-logger.go b/internal/logger/zap-logger.go index d8e2763..7ff638c 100644 --- a/internal/logger/zap-logger.go +++ b/internal/logger/zap-logger.go @@ -39,5 +39,5 @@ func getZapLogLevelByEnvLevel(envType config.EnvType) (zapcore.Level, error) { return zapcore.DebugLevel, nil } - return 0, fmt.Errorf("unexpected EnvType got (%d)", envType) + return 0, fmt.Errorf("unexpected EnvType got (%s)", envType) } diff --git a/internal/order/enrollment-worker.go b/internal/order/enrollment-worker.go index 4780a96..8ef591d 100644 --- a/internal/order/enrollment-worker.go +++ b/internal/order/enrollment-worker.go @@ -51,18 +51,21 @@ func (worker *EnrollmentWorker) requireOrder(enrollment *Enrollment) { retryAfter := resp.Header.Get("Retry-After") retryAfterInt, _ := strconv.Atoi(retryAfter) worker.logger.Info("got many request to accrual service", zap.Int("retry after", retryAfterInt)) + resp.Body.Close() time.Sleep(time.Second * time.Duration(retryAfterInt)) continue } if resp.StatusCode != http.StatusOK { worker.logger.Error("got unsuccessful status from accrual service", zap.String("status", resp.Status)) + resp.Body.Close() time.Sleep(time.Second * 5) continue } var buffer bytes.Buffer _, err = buffer.ReadFrom(resp.Body) + resp.Body.Close() if err != nil { worker.logger.Error("error while read response from accrual service", zap.String("order id", enrollment.OrderID)) time.Sleep(time.Second * 5) diff --git a/internal/user/map-repository.go b/internal/user/map-repository.go index c3e4774..5e8d952 100644 --- a/internal/user/map-repository.go +++ b/internal/user/map-repository.go @@ -27,9 +27,10 @@ func (repository *MapRepository) CreateUser(_ context.Context, user *User) error } func (repository *MapRepository) SetUserBalanceByID(_ context.Context, userID int, balance int64) error { - for _, user := range repository.data { + for i, user := range repository.data { if user.ID == userID { user.Balance = balance + repository.data[i] = user } } From 1c9b6d790348d546d7b6ccc082e9228bd7040176 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sun, 10 Dec 2023 14:58:58 +0200 Subject: [PATCH 28/29] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E,=20=D1=87.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gophermart/gophermart.go | 5 +-- internal/handler/auth-handler.go | 72 +++++++++++++++++++------------ internal/order/order.go | 3 +- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/internal/gophermart/gophermart.go b/internal/gophermart/gophermart.go index 567b0c5..046a671 100644 --- a/internal/gophermart/gophermart.go +++ b/internal/gophermart/gophermart.go @@ -30,10 +30,7 @@ func Start() error { } userController := user.NewController(user.CreatePGXRepository(db)) - authHandler := handler.AuthHandler{ - Logger: log, - UserController: userController, - } + authHandler := handler.NewAuthHandler(log, cfg.SecretKey, userController) router.Use(zip.GetZipMiddleware(log)) diff --git a/internal/handler/auth-handler.go b/internal/handler/auth-handler.go index efa5b7b..6780c22 100644 --- a/internal/handler/auth-handler.go +++ b/internal/handler/auth-handler.go @@ -34,10 +34,18 @@ type userJWTClaims struct { type AuthHandler struct { Logger *zap.Logger - SecretKey string + secretKey string UserController *user.Controller } +func NewAuthHandler(logger *zap.Logger, secretKey string, userController *user.Controller) *AuthHandler { + return &AuthHandler{ + Logger: logger, + secretKey: secretKey, + UserController: userController, + } +} + func (handler *AuthHandler) HandleAuthUser(writer http.ResponseWriter, request *http.Request) { var buffer bytes.Buffer @@ -63,11 +71,13 @@ func (handler *AuthHandler) HandleAuthUser(writer http.ResponseWriter, request * } fetchedUser, err := handler.UserController.GetUserByCredentials(request.Context(), credentials.Login, credentials.Password) - if err != nil && (errors.Is(err, user.ErrUserNotFound) || errors.Is(err, user.ErrIncorrectUserPassword)) { - handler.Logger.Debug("user send invalid credentials on login") - writer.WriteHeader(http.StatusUnauthorized) - return - } else if err != nil { + if err != nil { + if errors.Is(err, user.ErrUserNotFound) || errors.Is(err, user.ErrIncorrectUserPassword) { + handler.Logger.Debug("user send invalid credentials on login") + writer.WriteHeader(http.StatusUnauthorized) + return + } + handler.Logger.Error("error while get user on login", zap.Error(err)) writer.WriteHeader(http.StatusInternalServerError) return @@ -113,11 +123,13 @@ func (handler *AuthHandler) HandleRegisterUser(writer http.ResponseWriter, reque } createdUser, err := handler.UserController.AddUser(request.Context(), credentials.Login, credentials.Password) - if err != nil && errors.Is(err, user.ErrLoginAlreadyExists) { - handler.Logger.Debug("user try register existing account", zap.String("login", credentials.Login)) - writer.WriteHeader(http.StatusConflict) - return - } else if err != nil { + if err != nil { + if errors.Is(err, user.ErrLoginAlreadyExists) { + handler.Logger.Debug("user try register existing account", zap.String("login", credentials.Login)) + writer.WriteHeader(http.StatusConflict) + return + } + handler.Logger.Error("error while create new user by register", zap.Error(err)) writer.WriteHeader(http.StatusInternalServerError) return @@ -142,11 +154,13 @@ func (handler *AuthHandler) MiddlewareAuthorizeRequest() func(handler http.Handl return func(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { cookie, err := request.Cookie(authCookieName) - if err != nil && errors.Is(err, http.ErrNoCookie) { - handler.Logger.Debug("user has no auth cookie", zap.String("client address", request.RemoteAddr)) - writer.WriteHeader(http.StatusUnauthorized) - return - } else if err != nil { + if err != nil { + if errors.Is(err, http.ErrNoCookie) { + handler.Logger.Debug("user has no auth cookie", zap.String("client address", request.RemoteAddr)) + writer.WriteHeader(http.StatusUnauthorized) + return + } + handler.Logger.Error("undefined error occurred in auth middleware", zap.String("client address", request.RemoteAddr), zap.Error(err)) writer.WriteHeader(http.StatusInternalServerError) return @@ -154,17 +168,19 @@ func (handler *AuthHandler) MiddlewareAuthorizeRequest() func(handler http.Handl ctx := request.Context() authUser, err := handler.fetchUser(ctx, cookie.Value) - if err != nil && errors.Is(err, ErrWrongSign) { - handler.Logger.Debug("user sends invalid sign cookie", zap.Error(err)) - c := &http.Cookie{ - Value: "", - Name: authCookieName, - MaxAge: -1, + if err != nil { + if errors.Is(err, ErrWrongSign) { + handler.Logger.Debug("user sends invalid sign cookie", zap.Error(err)) + c := &http.Cookie{ + Value: "", + Name: authCookieName, + MaxAge: -1, + } + http.SetCookie(writer, c) + writer.WriteHeader(http.StatusUnauthorized) + return } - http.SetCookie(writer, c) - writer.WriteHeader(http.StatusUnauthorized) - return - } else if err != nil { + handler.Logger.Error("error while getting user for auth middleware", zap.Error(err)) writer.WriteHeader(http.StatusInternalServerError) return @@ -182,7 +198,7 @@ func (handler *AuthHandler) MiddlewareAuthorizeRequest() func(handler http.Handl func (handler *AuthHandler) fetchUser(ctx context.Context, sign string) (*user.User, error) { claims := &userJWTClaims{} _, err := jwt.ParseWithClaims(sign, claims, func(token *jwt.Token) (interface{}, error) { - return []byte(handler.SecretKey), nil + return []byte(handler.secretKey), nil }) if err != nil { @@ -207,7 +223,7 @@ func (handler *AuthHandler) createSign(signedUser *user.User) (string, error) { Login: signedUser.Login, }) - tokenString, err := token.SignedString([]byte(handler.SecretKey)) + tokenString, err := token.SignedString([]byte(handler.secretKey)) if err != nil { return "", err } diff --git a/internal/order/order.go b/internal/order/order.go index c5f7871..9c21102 100644 --- a/internal/order/order.go +++ b/internal/order/order.go @@ -1,6 +1,7 @@ package order import ( + "context" "errors" "strconv" ) @@ -10,7 +11,7 @@ var ErrEmptyBalance = errors.New("empty balance") func IsOrderNumberCorrect(orderNumber string) bool { sum := 0 - + context.Background() numSize := len(orderNumber) for i := 0; i < numSize; i++ { num, err := strconv.Atoi(string(orderNumber[numSize-i-1])) From af99b0774ee28192df7c6f8d8a8952787386e673 Mon Sep 17 00:00:00 2001 From: nessai1 Date: Sun, 10 Dec 2023 15:09:11 +0200 Subject: [PATCH 29/29] tests fixes --- internal/config/config.go | 9 ++++++++- internal/order/order.go | 2 -- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 6bf316e..e8cb777 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,6 +14,8 @@ const ( EnvTypeProduction EnvType = "production" ) +const defaultSecretKey = "default_secret_key" + type Config struct { ServiceAddr string AccrualServiceAddr string @@ -55,11 +57,16 @@ func fetchConfig() *Config { envType = EnvType(envTypeStr) } + secretKey := os.Getenv("ACCRUAL_SYSTEM_ADDRESS") + if secretKey == "" { + secretKey = defaultSecretKey + } + return &Config{ ServiceAddr: *serviceAddr, AccrualServiceAddr: *accrualServiceAddr, DBConnectionStr: *databaseConnection, - SecretKey: os.Getenv("ACCRUAL_SYSTEM_ADDRESS"), + SecretKey: secretKey, EnvType: envType, } } diff --git a/internal/order/order.go b/internal/order/order.go index 9c21102..7b95e3b 100644 --- a/internal/order/order.go +++ b/internal/order/order.go @@ -1,7 +1,6 @@ package order import ( - "context" "errors" "strconv" ) @@ -11,7 +10,6 @@ var ErrEmptyBalance = errors.New("empty balance") func IsOrderNumberCorrect(orderNumber string) bool { sum := 0 - context.Background() numSize := len(orderNumber) for i := 0; i < numSize; i++ { num, err := strconv.Atoi(string(orderNumber[numSize-i-1]))