diff --git a/cmd/gophermart/main.go b/cmd/gophermart/main.go index 38dd16da6..c150d549f 100644 --- a/cmd/gophermart/main.go +++ b/cmd/gophermart/main.go @@ -1,3 +1,31 @@ package main -func main() {} +import ( + "context" + "net/http" + + "github.com/Azcarot/GopherMarketProject/internal/router" + "github.com/Azcarot/GopherMarketProject/internal/storage" + "github.com/Azcarot/GopherMarketProject/internal/utils" +) + +func main() { + + flag := utils.ParseFlagsAndENV() + if flag.FlagDBAddr != "" { + err := storage.NewConn(flag) + if err != nil { + panic(err) + } + storage.CheckDBConnection(storage.DB) + storage.CreateTablesForGopherStore(storage.DB) + defer storage.DB.Close(context.Background()) + } + r := router.MakeRouter(flag) + server := &http.Server{ + Addr: flag.FlagAddr, + Handler: r, + } + server.ListenAndServe() + +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..cdf4047b8 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/Azcarot/GopherMarketProject + +go 1.21.4 + +require ( + github.com/caarlos0/env v3.5.0+incompatible // indirect + github.com/go-chi/chi/v5 v5.0.11 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // 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.3 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..b4e90c499 --- /dev/null +++ b/go.sum @@ -0,0 +1,27 @@ +github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= +github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= +github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +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.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= +github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +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= +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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/go.work b/go.work new file mode 100644 index 000000000..e202bde4e --- /dev/null +++ b/go.work @@ -0,0 +1,5 @@ +go 1.21.4 + +use ( + . +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 000000000..7cedd2164 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/handlers/balance.go b/internal/handlers/balance.go new file mode 100644 index 000000000..20a5f3e3c --- /dev/null +++ b/internal/handlers/balance.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/Azcarot/GopherMarketProject/internal/storage" +) + +func GetBalance() http.Handler { + balance := func(res http.ResponseWriter, req *http.Request) { + // var buf bytes.Buffer + token := req.Header.Get("Authorization") + claims, ok := storage.VerifyToken(token) + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + var userData storage.UserData + userData.Login = claims["sub"].(string) + ok, err := storage.CheckUserExists(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + var balanceData storage.BalanceResponce + balanceData, err = storage.GetUserBalance(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + balanceData.Accrual = balanceData.Accrual / 100 + balanceData.Withdrawn = balanceData.Withdrawn / 100 + result, err := json.Marshal(balanceData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.Header().Add("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(result) + } + return http.HandlerFunc(balance) +} diff --git a/internal/handlers/getorders.go b/internal/handlers/getorders.go new file mode 100644 index 000000000..f02d6ed29 --- /dev/null +++ b/internal/handlers/getorders.go @@ -0,0 +1,151 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "strconv" + "sync" + "time" + + "github.com/Azcarot/GopherMarketProject/internal/storage" + "github.com/Azcarot/GopherMarketProject/internal/utils" + "github.com/jackc/pgx/v5" +) + +func GetOrders() http.Handler { + getorder := func(res http.ResponseWriter, req *http.Request) { + // var buf bytes.Buffer + token := req.Header.Get("Authorization") + claims, ok := storage.VerifyToken(token) + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + var userData storage.UserData + userData.Login = claims["sub"].(string) + ok, err := storage.CheckUserExists(storage.DB, userData) + + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + orders, err := storage.GetCustomerOrders(storage.DB, userData.Login) + if err == pgx.ErrNoRows { + res.WriteHeader(http.StatusNoContent) + return + } + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + result, err := json.Marshal(orders) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.Header().Add("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(result) + + } + return http.HandlerFunc(getorder) +} +func GetOrderData(flag utils.Flags, order uint64) (OrderRequest, error) { + pth := flag.FlagAccrualAddr + "/api/orders/" + strconv.Itoa(int(order)) + var b []byte + result := OrderRequest{} + resp, err := http.NewRequest("GET", pth, bytes.NewBuffer(b)) + if err != nil { + return result, err + } + + var res *http.Response + res, err = CheckStatus(resp) + if err != nil { + return result, err + } + defer res.Body.Close() + + var buf bytes.Buffer + // читаем тело запроса + _, err = buf.ReadFrom(res.Body) + if err != nil { + return result, err + } + data := buf.Bytes() + + if err = json.Unmarshal(data, &result); err != nil { + return result, err + } + return result, err +} + +func CheckStatus(resp *http.Request) (*http.Response, error) { + client := &http.Client{} + res, err := client.Do(resp) + if err != nil { + return res, err + } + if res.StatusCode == http.StatusTooManyRequests { + time.Sleep(time.Duration(1 * time.Second)) + res, _ = CheckStatus(resp) + + } + return res, err +} + +func ActualiseOrders(flag utils.Flags, quit chan struct{}) { + orderNumbers, err := storage.GetUnfinishedOrders(storage.DB) + if err != nil { + time.Sleep(time.Duration(time.Duration(5).Seconds())) + orderNumbers, err = storage.GetUnfinishedOrders(storage.DB) + if err != nil { + return + } + } + var wg sync.WaitGroup + for i, order := range orderNumbers { + ind := i + ord := order + wg.Add(1) + go func(int, uint64) { + defer wg.Done() + orderReq, err := GetOrderData(flag, ord) + if err != nil { + return + } + if (orderReq.Status != "NEW") && (orderReq.Status != "PROCESSING") { + var orderData storage.OrderData + orderData.Accrual = int(orderReq.Accrual * 100) + orderNumber, err := strconv.Atoi(orderReq.OrderNumber) + if err != nil { + return + } + orderData.OrderNumber = uint64(orderNumber) + orderData.State = orderReq.Status + err = storage.UpdateOrder(storage.DB, orderData) + if err != nil { + return + } + if orderData.Accrual > 0 { + _, err := storage.AddBalanceToUser(storage.DB, orderData) + if err != nil { + return + } + } + orderNumbers[ind] = orderNumbers[len(orderNumbers)-1] + orderNumbers = orderNumbers[:len(orderNumbers)-1] + } + if len(orderNumbers) == 0 { + return + } + }(ind, ord) + } + wg.Wait() + +} diff --git a/internal/handlers/getwithdrawals.go b/internal/handlers/getwithdrawals.go new file mode 100644 index 000000000..cabd0d8a2 --- /dev/null +++ b/internal/handlers/getwithdrawals.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/Azcarot/GopherMarketProject/internal/storage" +) + +func GetWithdrawals() http.Handler { + withdraw := func(res http.ResponseWriter, req *http.Request) { + // var buf bytes.Buffer + token := req.Header.Get("Authorization") + claims, ok := storage.VerifyToken(token) + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + var userData storage.UserData + userData.Login = claims["sub"].(string) + ok, err := storage.CheckUserExists(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + withdrawals, err := storage.GetWithdrawals(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + result, err := json.Marshal(withdrawals) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.Header().Add("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(result) + } + return http.HandlerFunc(withdraw) +} diff --git a/internal/handlers/login.go b/internal/handlers/login.go new file mode 100644 index 000000000..37c292ba6 --- /dev/null +++ b/internal/handlers/login.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "time" + + "github.com/Azcarot/GopherMarketProject/internal/storage" + "github.com/golang-jwt/jwt" +) + +// Структура HTTP-запроса на вход в аккаунт +type LoginRequest struct { + Login string `json:"login"` + Password string `json:"password"` +} + +// Структура HTTP-ответа на вход в аккаунт +// В ответе содержится JWT-токен авторизованного пользователя +type LoginResponse struct { + AccessToken string `json:"access_token"` +} + +var jwtSecretKey = []byte(storage.SecretKey) + +func LoginUser() http.Handler { + login := func(res http.ResponseWriter, req *http.Request) { + var buf bytes.Buffer + loginData := LoginRequest{} + // читаем тело запроса + _, err := buf.ReadFrom(req.Body) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + data := buf.Bytes() + + if err = json.Unmarshal(data, &loginData); err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + var userData storage.UserData + userData.Login = loginData.Login + userData.Password = loginData.Password + result, err := storage.CheckUserPassword(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + if !result { + res.WriteHeader(http.StatusUnauthorized) + return + } + payload := jwt.MapClaims{ + "sub": loginData.Login, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Создаем новый JWT-токен и подписываем его по алгоритму HS256 + token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) + + authToken, err := token.SignedString(jwtSecretKey) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.Header().Add("Authorization", authToken) + + res.WriteHeader(http.StatusOK) + + } + return http.HandlerFunc(login) +} diff --git a/internal/handlers/order.go b/internal/handlers/order.go new file mode 100644 index 000000000..94d3593ed --- /dev/null +++ b/internal/handlers/order.go @@ -0,0 +1,79 @@ +package handlers + +import ( + "io" + "net/http" + "strconv" + "time" + + "github.com/Azcarot/GopherMarketProject/internal/storage" + "github.com/Azcarot/GopherMarketProject/internal/utils" +) + +type OrderRequest struct { + OrderNumber string `json:"order"` + Status string `json:"status"` + Accrual float64 `json:"accrual"` +} + +func Order(flag utils.Flags) http.Handler { + order := func(res http.ResponseWriter, req *http.Request) { + // var buf bytes.Buffer + token := req.Header.Get("Authorization") + claims, ok := storage.VerifyToken(token) + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + var userData storage.UserData + userData.Login = claims["sub"].(string) + ok, err := storage.CheckUserExists(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + // читаем тело запроса + data, err := io.ReadAll(req.Body) + asString := string(data) + // _, err = buf.ReadFrom(req.Body) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + // data := buf.Bytes() + orderNumber, err := strconv.ParseUint(asString, 10, 64) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + ok = utils.IsOrderNumberValid(orderNumber) + if !ok { + res.WriteHeader(http.StatusUnprocessableEntity) + return + } + var order storage.OrderData + order.OrderNumber = orderNumber + order.User = userData.Login + ok, anotherUser := storage.CheckIfOrderExists(storage.DB, order) + if anotherUser { + res.WriteHeader(http.StatusConflict) + return + } + if !ok { + res.WriteHeader(http.StatusOK) + return + } + order.Date = time.Now().Format(time.RFC3339) + err = storage.CreateNewOrder(storage.DB, order) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.WriteHeader(http.StatusAccepted) + } + return http.HandlerFunc(order) +} diff --git a/internal/handlers/regstration.go b/internal/handlers/regstration.go new file mode 100644 index 000000000..32929bf1d --- /dev/null +++ b/internal/handlers/regstration.go @@ -0,0 +1,77 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "time" + + "github.com/Azcarot/GopherMarketProject/internal/storage" + "github.com/golang-jwt/jwt" +) + +type RegisterRequest struct { + Login string `json:"login"` + Password string `json:"password"` +} + +type Payload struct { + Login string + Exp int64 +} + +func Registration() http.Handler { + register := func(res http.ResponseWriter, req *http.Request) { + var buf bytes.Buffer + regData := RegisterRequest{} + // читаем тело запроса + _, err := buf.ReadFrom(req.Body) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + data := buf.Bytes() + + if err = json.Unmarshal(data, ®Data); err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + userData := storage.UserData{} + userData.Login = regData.Login + userData.Password = regData.Password + userData.Date = time.Now().Format(time.RFC3339) + result, err := storage.CheckUserExists(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + if result { + res.WriteHeader(http.StatusConflict) + return + } + err = storage.CreateNewUser(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + payloadData := Payload{} + payloadData.Login = userData.Login + payloadData.Exp = time.Now().Add(time.Hour * 72).Unix() + payload := jwt.MapClaims{ + "sub": payloadData.Login, + "exp": payloadData.Exp, + } + + // Создаем новый JWT-токен и подписываем его по алгоритму HS256 + token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) + + authToken, err := token.SignedString(jwtSecretKey) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.Header().Add("Authorization", authToken) + res.WriteHeader(http.StatusOK) + } + return http.HandlerFunc(register) +} diff --git a/internal/handlers/withdraw.go b/internal/handlers/withdraw.go new file mode 100644 index 000000000..e289949dd --- /dev/null +++ b/internal/handlers/withdraw.go @@ -0,0 +1,91 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/Azcarot/GopherMarketProject/internal/storage" + "github.com/Azcarot/GopherMarketProject/internal/utils" +) + +func Withdraw() http.Handler { + withdraw := func(res http.ResponseWriter, req *http.Request) { + // var buf bytes.Buffer + token := req.Header.Get("Authorization") + claims, ok := storage.VerifyToken(token) + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + var userData storage.UserData + var orderData storage.OrderData + userData.Login = claims["sub"].(string) + ok, err := storage.CheckUserExists(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + if !ok { + res.WriteHeader(http.StatusUnauthorized) + return + } + var buf bytes.Buffer + withdrawalData := storage.WithdrawRequest{} + // читаем тело запроса + _, err = buf.ReadFrom(req.Body) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + data := buf.Bytes() + + if err = json.Unmarshal(data, &withdrawalData); err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + orderNumber, err := strconv.ParseUint(withdrawalData.OrderNumber, 10, 64) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + return + } + ok = utils.IsOrderNumberValid(orderNumber) + if !ok { + res.WriteHeader(http.StatusUnprocessableEntity) + return + } + var balanceData storage.BalanceResponce + balanceData, err = storage.GetUserBalance(storage.DB, userData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + intAccBalanceData := int(balanceData.Accrual) + intWithdData := int(balanceData.Withdrawn) + if int(withdrawalData.Amount*100) > intAccBalanceData { + res.WriteHeader(http.StatusPaymentRequired) + return + } + userData.AccrualPoints = intAccBalanceData + userData.Withdrawal = intWithdData + err = storage.WitdrawFromUser(storage.DB, userData, withdrawalData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + orderData.Accrual = 0 + orderData.OrderNumber, _ = strconv.ParseUint(withdrawalData.OrderNumber, 10, 64) + orderData.Withdrawal = int(withdrawalData.Amount * 100) + orderData.Date = time.Now().Format(time.RFC3339) + orderData.User = userData.Login + err = storage.CreateNewOrder(storage.DB, orderData) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.WriteHeader(http.StatusOK) + } + return http.HandlerFunc(withdraw) +} diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go new file mode 100644 index 000000000..37ff09bba --- /dev/null +++ b/internal/middleware/logger.go @@ -0,0 +1,68 @@ +package middleware + +import ( + "net/http" + "time" + + "go.uber.org/zap" +) + +var Sugar zap.SugaredLogger + +type ( + // берём структуру для хранения сведений об ответе + responseData struct { + status int + size int + } + + // добавляем реализацию http.ResponseWriter + loggingResponseWriter struct { + http.ResponseWriter // встраиваем оригинальный http.ResponseWriter + responseData *responseData + } +) + +func (r *loggingResponseWriter) Write(b []byte) (int, error) { + // записываем ответ, используя оригинальный http.ResponseWriter + size, err := r.ResponseWriter.Write(b) + r.responseData.size += size // захватываем размер + return size, err +} + +func (r *loggingResponseWriter) WriteHeader(statusCode int) { + // записываем код статуса, используя оригинальный http.ResponseWriter + r.ResponseWriter.WriteHeader(statusCode) + r.responseData.status = statusCode // захватываем код статуса +} + +// WithLogging добавляет дополнительный код для регистрации сведений о запросе +// и возвращает новый http.Handler. +func WithLogging(h http.Handler) http.Handler { + logFn := func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + responseData := &responseData{ + status: 0, + size: 0, + } + lw := loggingResponseWriter{ + ResponseWriter: w, // встраиваем оригинальный http.ResponseWriter + responseData: responseData, + } + h.ServeHTTP(&lw, r) // внедряем реализацию http.ResponseWriter + + duration := time.Since(start) + + Sugar.Infoln( + "uri", r.RequestURI, + "method", r.Method, + "body", r.Body, + "status", responseData.status, // получаем перехваченный код статуса ответа + "duration", duration, + "size", responseData.size, // получаем перехваченный размер ответа + ) + } + // возвращаем функционально расширенный хендлер + return http.HandlerFunc(logFn) +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 000000000..046a2a284 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,50 @@ +package router + +import ( + "time" + + "github.com/Azcarot/GopherMarketProject/internal/handlers" + "github.com/Azcarot/GopherMarketProject/internal/middleware" + "github.com/Azcarot/GopherMarketProject/internal/utils" + "github.com/go-chi/chi/v5" + "go.uber.org/zap" +) + +var Flag utils.Flags + +func MakeRouter(flag utils.Flags) *chi.Mux { + + logger, err := zap.NewDevelopment() + if err != nil { + // вызываем панику, если ошибка + panic(err) + } + defer logger.Sync() + // делаем регистратор SugaredLogger + middleware.Sugar = *logger.Sugar() + r := chi.NewRouter() + ticker := time.NewTicker(2 * time.Second) + quit := make(chan struct{}) + go func() { + for { + select { + case <-ticker.C: + handlers.ActualiseOrders(flag, quit) + case <-quit: + ticker.Stop() + return + } + } + }() + r.Use(middleware.WithLogging) + r.Route("/api/user", func(r chi.Router) { + r.Post("/register", handlers.Registration().ServeHTTP) + r.Post("/login", handlers.LoginUser().ServeHTTP) + r.Post("/orders", handlers.Order(flag).ServeHTTP) + r.Post("/balance/withdraw", handlers.Withdraw().ServeHTTP) + r.Get("/orders", handlers.GetOrders().ServeHTTP) + r.Get("/balance", handlers.GetBalance().ServeHTTP) + r.Get("/withdrawals", handlers.GetWithdrawals().ServeHTTP) + }) + return r +} diff --git a/internal/storage/postgressql.go b/internal/storage/postgressql.go new file mode 100644 index 000000000..719347fa8 --- /dev/null +++ b/internal/storage/postgressql.go @@ -0,0 +1,382 @@ +package storage + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "time" + + "github.com/Azcarot/GopherMarketProject/internal/utils" + "github.com/golang-jwt/jwt" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +const SecretKey string = "super-secret" + +type MyCustomClaims struct { + jwt.MapClaims +} + +type UserData struct { + Login string + Password string + AccrualPoints int + Withdrawal int + Date string +} +type OrderData struct { + OrderNumber uint64 `json:"number"` + Accrual int `json:"accrual"` + User string + State string `json:"status"` + Date string `json:"uploaded_at"` + Withdrawal int +} + +type OrderResponse struct { + OrderNumber string `json:"number"` + Accrual float64 `json:"accrual"` + State string `json:"status"` + Date string `json:"uploaded_at"` +} + +type BalanceResponce struct { + Accrual float64 `json:"current"` + Withdrawn float64 `json:"withdrawn"` +} + +var DB *pgx.Conn + +type pgxConnTime struct { + attempts int + timeBeforeAttempt int +} + +type WithdrawRequest struct { + OrderNumber string `json:"order"` + Amount float64 `json:"sum"` +} + +type WithdrawResponse struct { + OrderNumber string `json:"order"` + Amount float64 `json:"sum"` + ProcessedAt string `json:"processed_at"` +} + +func NewConn(f utils.Flags) error { + var err error + var attempts pgxConnTime + attempts.attempts = 3 + attempts.timeBeforeAttempt = 1 + err = connectToDB(f) + for err != nil { + //если ошибка связи с бд, то это не эскпортируемый тип, отличный от PgError + var pqErr *pgconn.PgError + if errors.Is(err, pqErr) { + return err + + } + if attempts.attempts == 0 { + return err + } + times := time.Duration(attempts.timeBeforeAttempt) + time.Sleep(times * time.Second) + attempts.attempts -= 1 + attempts.timeBeforeAttempt += 2 + err = connectToDB(f) + + } + return nil +} + +func connectToDB(f utils.Flags) error { + var err error + ps := fmt.Sprintf(f.FlagDBAddr) + DB, err = pgx.Connect(context.Background(), ps) + return err +} + +func CheckDBConnection(db *pgx.Conn) http.Handler { + checkConnection := func(res http.ResponseWriter, req *http.Request) { + + err := DB.Ping(context.Background()) + result := (err == nil) + if result { + res.WriteHeader(http.StatusOK) + } else { + res.WriteHeader(http.StatusInternalServerError) + } + + } + return http.HandlerFunc(checkConnection) +} + +func CreateTablesForGopherStore(db *pgx.Conn) { + ctx := context.Background() + + queryForFun := `DROP TABLE IF EXISTS users CASCADE` + db.Exec(ctx, queryForFun) + query := `CREATE TABLE IF NOT EXISTS users ( + id SERIAL NOT NULL PRIMARY KEY, + login text NOT NULL, + password text NOT NULL, + accrual_points bigint NOT NULL, + withdrawal BIGINT NOT NULL, + created text )` + + _, err := db.Exec(ctx, query) + + if err != nil { + + log.Printf("Error %s when creating user table", err) + + } + queryForFun = `DROP TABLE IF EXISTS orders CASCADE` + db.Exec(ctx, queryForFun) + query = `CREATE TABLE IF NOT EXISTS orders( + id SERIAL NOT NULL PRIMARY KEY, + order_number BIGINT, + accrual_points BIGINT NOT NULL, + state TEXT, + withdrawal BIGINT NOT NULL, + customer TEXT NOT NULL, + created TEXT + )` + _, err = db.Exec(ctx, query) + + if err != nil { + + log.Printf("Error %s when creating order table", err) + + } +} + +func CreateNewUser(db *pgx.Conn, data UserData) error { + ctx := context.Background() + encodedPW := utils.ShaData(data.Password, SecretKey) + _, err := db.Exec(ctx, `INSERT into users (login, password, accrual_points, withdrawal, created) + values ($1, $2, $3, $4, $5);`, + data.Login, encodedPW, 0, 0, data.Date) + return err +} + +func CheckUserExists(db *pgx.Conn, data UserData) (bool, error) { + ctx := context.Background() + var login string + sqlQuery := fmt.Sprintf(`SELECT login FROM users WHERE login = '%s'`, data.Login) + err := db.QueryRow(ctx, sqlQuery).Scan(&login) + + if err == pgx.ErrNoRows { + + return false, nil + } + + if err != nil { + return false, err + } + + return true, nil + +} + +func CheckUserPassword(db *pgx.Conn, data UserData) (bool, error) { + encodedPw := utils.ShaData(data.Password, SecretKey) + ctx := context.Background() + sqlQuery := fmt.Sprintf(`SELECT login, password FROM users WHERE login = '%s'`, data.Login) + var login, pw string + err := db.QueryRow(ctx, sqlQuery).Scan(&login, &pw) + if err != nil { + return false, err + } + + if encodedPw != pw { + return false, nil + } + return true, nil +} + +func CreateNewOrder(db *pgx.Conn, data OrderData) error { + ctx := context.Background() + data.State = "NEW" + _, err := db.Exec(ctx, `INSERT INTO orders + (order_number, accrual_points, state, customer, withdrawal, created) + values ($1, $2, $3, $4, $5, $6);`, + data.OrderNumber, data.Accrual, data.State, data.User, data.Withdrawal, data.Date) + return err +} + +func VerifyToken(token string) (jwt.MapClaims, bool) { + hmacSecretString := SecretKey + hmacSecret := []byte(hmacSecretString) + gettoken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + return hmacSecret, nil + }) + + if err != nil { + return nil, false + } + + if claims, ok := gettoken.Claims.(jwt.MapClaims); ok && gettoken.Valid { + return claims, true + + } else { + log.Printf("Invalid JWT Token") + return nil, false + } +} + +func GetCustomerOrders(db *pgx.Conn, login string) ([]OrderResponse, error) { + query := fmt.Sprintf(`SELECT order_number, accrual_points, state, created + FROM orders + WHERE customer = '%s' + ORDER BY id DESC`, login) + result := []OrderResponse{} + ctx := context.Background() + rows, err := db.Query(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var order OrderResponse + if err := rows.Scan(&order.OrderNumber, &order.Accrual, &order.State, &order.Date); err != nil { + return result, err + } + order.Accrual = order.Accrual / 100 + result = append(result, order) + } + if err = rows.Err(); err != nil { + return result, err + } + return result, nil + +} + +func CheckIfOrderExists(db *pgx.Conn, data OrderData) (bool, bool) { + query := fmt.Sprintf(`SELECT order_number, customer + FROM orders + WHERE order_number = %d`, data.OrderNumber) + ctx := context.Background() + var number uint64 + var login string + err := db.QueryRow(ctx, query).Scan(&number, &login) + if err == pgx.ErrNoRows { + //No order + return true, false + } + // Order exists for another user + if login != data.User { + return false, true + } + // order already exists for current user + return false, false +} + +func GetUnfinishedOrders(db *pgx.Conn) ([]uint64, error) { + sqlQuery := "SELECT order_number FROM orders WHERE state IN ('NEW', 'PROCESSING')" + ctx := context.Background() + var result []uint64 + rows, err := db.Query(ctx, sqlQuery) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var order uint64 + if err := rows.Scan(&order); err != nil { + return result, err + } + result = append(result, order) + } + if err = rows.Err(); err != nil { + return result, err + } + return result, nil + +} + +func UpdateOrder(db *pgx.Conn, data OrderData) error { + ctx := context.Background() + sql := ` + UPDATE orders + SET accrual_points = $1, state = $2 + WHERE order_number = $3; +` + _, err := db.Exec(ctx, sql, data.Accrual, data.State, data.OrderNumber) + return err +} + +func AddBalanceToUser(db *pgx.Conn, orderData OrderData) (bool, error) { + ctx := context.Background() + sqlQuery := fmt.Sprintf(`SELECT users.accrual_points, users.login + FROM users + LEFT JOIN orders + ON users.login = orders.customer + WHERE orders.order_number = '%d'`, orderData.OrderNumber) + var currentBalance int + var login string + err := db.QueryRow(ctx, sqlQuery).Scan(¤tBalance, &login) + if err != nil { + return false, err + } + currentBalance += orderData.Accrual + sql := `UPDATE users SET accrual_points = $1 WHERE login = $2` + _, err = db.Exec(ctx, sql, currentBalance, login) + if err != nil { + return false, err + } + return true, err +} + +func GetUserBalance(db *pgx.Conn, data UserData) (BalanceResponce, error) { + sql := fmt.Sprintf(`SELECT accrual_points, withdrawal FROM users WHERE login = '%s'`, data.Login) + ctx := context.Background() + var result BalanceResponce + err := db.QueryRow(ctx, sql).Scan(&result.Accrual, &result.Withdrawn) + if err != nil { + return result, err + } + + return result, err +} + +func WitdrawFromUser(db *pgx.Conn, userData UserData, withdraw WithdrawRequest) error { + ctx := context.Background() + currentBalance := userData.AccrualPoints + fmt.Println("userData", userData) + fmt.Println("withdraw", withdraw) + currentBalance -= int(withdraw.Amount * 100) + currentWithdrawn := userData.Withdrawal + int(withdraw.Amount*100) + sql := `UPDATE users SET accrual_points = $1, withdrawal = $2 WHERE login = $3` + _, err := db.Exec(ctx, sql, currentBalance, currentWithdrawn, userData.Login) + if err != nil { + return err + } + return nil +} + +func GetWithdrawals(db *pgx.Conn, userData UserData) ([]WithdrawResponse, error) { + var result []WithdrawResponse + sqlQuery := fmt.Sprintf(`SELECT order_number, withdrawal, created FROM orders WHERE customer = '%s' and withdrawal > 0 ORDER BY id DESC`, userData.Login) + ctx := context.Background() + rows, err := db.Query(ctx, sqlQuery) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var order WithdrawResponse + if err := rows.Scan(&order.OrderNumber, &order.Amount, &order.ProcessedAt); err != nil { + return result, err + } + order.Amount = order.Amount / 100 + result = append(result, order) + } + if err = rows.Err(); err != nil { + return result, err + } + return result, nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 000000000..8de9530f5 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,84 @@ +package utils + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "flag" + "log" + + "github.com/caarlos0/env" +) + +type Flags struct { + FlagAddr string + FlagDBAddr string + FlagAccrualAddr string +} + +type ServerENV struct { + Address string `env:"RUN_ADDRESS"` + DBAddress string `env:"DATABASE_URI"` + AccrualAddr string `env:"ACCRUAL_SYSTEM_ADDRESS"` +} + +func ShaData(result string, key string) string { + b := []byte(result) + shakey := []byte(key) + // создаём новый hash.Hash, вычисляющий контрольную сумму SHA-256 + h := hmac.New(sha256.New, shakey) + // передаём байты для хеширования + h.Write(b) + // вычисляем хеш + hash := h.Sum(nil) + sha := base64.URLEncoding.EncodeToString(hash) + return string(sha) +} + +func ParseFlagsAndENV() Flags { + var Flag Flags + flag.StringVar(&Flag.FlagAddr, "a", "localhost:8080", "address and port to run server") + flag.StringVar(&Flag.FlagDBAddr, "d", "", "address for db") + flag.StringVar(&Flag.FlagAccrualAddr, "r", "", "accrual system addr") + flag.Parse() + var envcfg ServerENV + err := env.Parse(&envcfg) + if err != nil { + log.Fatal(err) + } + + if len(envcfg.Address) > 0 { + Flag.FlagAddr = envcfg.Address + } + if len(envcfg.DBAddress) > 0 { + Flag.FlagDBAddr = envcfg.DBAddress + } + + if len(envcfg.AccrualAddr) > 0 { + Flag.FlagAccrualAddr = envcfg.AccrualAddr + } + + return Flag +} + +func IsOrderNumberValid(number uint64) bool { + return (number%10+orderChecksum(number/10))%10 == 0 +} + +func orderChecksum(number uint64) uint64 { + var luhn uint64 + for i := 0; number > 0; i++ { + cur := number % 10 + + if i%2 == 0 { // even + cur = cur * 2 + if cur > 9 { + cur = cur%10 + cur/10 + } + } + + luhn += cur + number = number / 10 + } + return luhn % 10 +}