diff --git a/.gitignore b/.gitignore index 50d43ce..39e9857 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ # Binaries cmd/gophermart/main cmd/gophermart/gophermart +bin/ # Dependency directories (remove the comment below to include it) vendor/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d22a75a --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +APP=./bin/gophermart +ACCRUAL=./cmd/accrual/accrual_linux_amd64 +PORT=8080 +ADDRESS=localhost +DSN='postgresql://postgres:postgres@localhost:5432/gophermart?sslmode=disable' + +.PHONY: build +build: + go test -v -cover ./... + go build -o ${APP} ./cmd/gophermart/... + +.PHONY: test +test: build + gophermarttest -test.v -test.run="^TestGophermart$$" \ + -gophermart-binary-path=${APP} \ + -gophermart-host=${ADDRESS} \ + -gophermart-port=${PORT} \ + -gophermart-database-uri=${DSN} \ + -accrual-binary-path=${ACCRUAL} \ + -accrual-host=${ADDRESS} \ + -accrual-port=34567 \ + -accrual-database-uri=${DSN} diff --git a/cmd/gophermart/main.go b/cmd/gophermart/main.go index 38dd16d..9e020f0 100644 --- a/cmd/gophermart/main.go +++ b/cmd/gophermart/main.go @@ -1,3 +1,38 @@ package main -func main() {} +import ( + "flag" + "os" + + _ "github.com/jackc/pgx/v5/stdlib" + + "github.com/OlegVankov/fantastic-engine/internal" +) + +var ( + serverAddr string + accrualAddr string + databaseURI string +) + +func main() { + + flag.StringVar(&serverAddr, "a", "localhost:8080", "адрес и порт запуска сервиса") + flag.StringVar(&accrualAddr, "r", "http://localhost:34567", "адрес системы расчёта начислений") + flag.StringVar(&databaseURI, "d", "postgresql://postgres:postgres@localhost:5432/gophermart?sslmode=disable", "адрес подключения к базе данных") + + flag.Parse() + + if envRunAddr := os.Getenv("RUN_ADDRESS"); envRunAddr != "" { + serverAddr = envRunAddr + } + if envAccrualAddr := os.Getenv("ACCRUAL_SYSTEM_ADDRESS"); envAccrualAddr != "" { + accrualAddr = envAccrualAddr + } + if envDatabaseURI := os.Getenv("DATABASE_URI"); envDatabaseURI != "" { + databaseURI = envDatabaseURI + } + + server := internal.NewServer(serverAddr, databaseURI) + server.Run(accrualAddr) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0e0d161 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/OlegVankov/fantastic-engine + +go 1.20 + +require ( + github.com/go-chi/chi/v5 v5.0.11 // indirect + github.com/go-resty/resty/v2 v2.11.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jackc/pgx/v5 v5.5.1 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8bd9c19 --- /dev/null +++ b/go.sum @@ -0,0 +1,74 @@ +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/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= +github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +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/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-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= +github.com/jackc/pgx/v5 v5.5.1/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/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/handler/accrual/accrual.go b/internal/handler/accrual/accrual.go new file mode 100644 index 0000000..0198b41 --- /dev/null +++ b/internal/handler/accrual/accrual.go @@ -0,0 +1,57 @@ +package accrual + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/OlegVankov/fantastic-engine/internal/handler" +) + +func SendAccrual(addr string, handler *handler.Handler) { + ctx := context.Background() + client := resty.New() + url := addr + "/api/orders/" + ball := struct { + Order string `json:"order"` + Status string `json:"status"` + Accrual float64 `json:"accrual"` + }{} + + timer := time.NewTimer(time.Duration(5) * time.Second) + defer timer.Stop() + + for range timer.C { + + orders, err := handler.Repository.GetOrders(ctx) + + if err != nil { + continue + } + + for _, k := range orders { + url := url + k.Number + resp, err := client.R(). + SetResult(&ball). + Get(url) + if err != nil { + fmt.Printf("[ERROR] %s\n", err.Error()) + } + + if resp.StatusCode() == http.StatusOK { + + err := handler.Repository.UpdateOrder(ctx, ball.Order, ball.Status, ball.Accrual) + if err != nil { + fmt.Printf("[ERROR] %s\n", err.Error()) + continue + } + + } + + } + + } +} diff --git a/internal/handler/checkauth.go b/internal/handler/checkauth.go new file mode 100644 index 0000000..9a09224 --- /dev/null +++ b/internal/handler/checkauth.go @@ -0,0 +1,21 @@ +package handler + +import ( + "net/http" + "strings" + + "github.com/OlegVankov/fantastic-engine/internal/util/jwt" +) + +func Auth(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + username := jwt.GetUser(token) + if username == "" { + w.WriteHeader(http.StatusUnauthorized) + return + } + r.Header.Add("username", username) + h.ServeHTTP(w, r) + }) +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go new file mode 100644 index 0000000..d54b1af --- /dev/null +++ b/internal/handler/handler.go @@ -0,0 +1,236 @@ +package handler + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + + "github.com/OlegVankov/fantastic-engine/internal/repository" + "github.com/OlegVankov/fantastic-engine/internal/util/hash" + "github.com/OlegVankov/fantastic-engine/internal/util/jwt" + "github.com/OlegVankov/fantastic-engine/internal/util/lun" +) + +type Handler struct { + Repository repository.Repository +} + +type credential struct { + Login string `json:"login"` + Password string `json:"password"` +} + +func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { + + c := credential{} + + err := json.NewDecoder(r.Body).Decode(&c) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + pass, err := hash.StringToHash(c.Password) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + user, err := h.Repository.AddUser(r.Context(), c.Login, pass) + if err != nil { + var e *pgconn.PgError + if errors.As(err, &e) && e.Code == "23505" { + w.WriteHeader(http.StatusConflict) + return + } + fmt.Printf("%v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + tkn, err := jwt.CreateToken(user.Login, user.ID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + authorization := fmt.Sprintf("Bearer %s", tkn) + + w.Header().Add("Authorization", authorization) + w.WriteHeader(http.StatusOK) +} + +func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { + c := credential{} + + err := json.NewDecoder(r.Body).Decode(&c) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + user, err := h.Repository.GetUser(r.Context(), c.Login) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + + if !hash.CheckPassword(user.Password, c.Password) { + w.WriteHeader(http.StatusUnauthorized) + return + } + + tkn, err := jwt.CreateToken(user.Login, user.ID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + authorization := fmt.Sprintf("Bearer %s", tkn) + + w.Header().Add("Authorization", authorization) + w.WriteHeader(http.StatusOK) +} + +func (h *Handler) Orders(w http.ResponseWriter, r *http.Request) { + + body, err := io.ReadAll(r.Body) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if r.Header.Get("Content-Type") != "text/plain" { + w.WriteHeader(http.StatusBadRequest) + } + + number := string(body) + + if !lun.CheckLun(number) { + w.WriteHeader(http.StatusUnprocessableEntity) + return + } + + username := r.Header.Get("username") + + _, err = h.Repository.AddOrder(r.Context(), username, number) + if err != nil { + var e *pgconn.PgError + if errors.As(err, &e) && e.Code == "23505" { + order, err := h.Repository.GetOrderByNumber(r.Context(), number) + if err == nil { + if order.UserLogin == username { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusConflict) + return + } + } + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusAccepted) +} + +func (h *Handler) GetOrders(w http.ResponseWriter, r *http.Request) { + username := r.Header.Get("username") + + orders, err := h.Repository.GetOrdersByLogin(r.Context(), username) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if len(orders) == 0 { + w.WriteHeader(http.StatusNoContent) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(orders) +} + +func (h *Handler) Withdraw(w http.ResponseWriter, r *http.Request) { + username := r.Header.Get("username") + withdraw := struct { + Order string + Sum float64 + }{} + err := json.NewDecoder(r.Body).Decode(&withdraw) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if !lun.CheckLun(withdraw.Order) { + w.WriteHeader(http.StatusUnprocessableEntity) + return + } + + err = h.Repository.UpdateWithdraw(r.Context(), username, withdraw.Order, withdraw.Sum) + if err != nil { + if err.Error() == "balance error" { + w.WriteHeader(http.StatusPaymentRequired) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + +func (h *Handler) Balance(w http.ResponseWriter, r *http.Request) { + username := r.Header.Get("username") + + user, err := h.Repository.GetBalance(r.Context(), username) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + balance := struct { + Current float64 + Withdrawn float64 + }{ + user.Balance, + user.Withdraw, + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(balance) +} + +func (h *Handler) Withdrawals(w http.ResponseWriter, r *http.Request) { + username := r.Header.Get("username") + + wd, err := h.Repository.GetWithdrawals(r.Context(), username) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if len(wd) == 0 { + w.WriteHeader(http.StatusNoContent) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(wd) +} diff --git a/internal/model/models.go b/internal/model/models.go new file mode 100644 index 0000000..2287039 --- /dev/null +++ b/internal/model/models.go @@ -0,0 +1,27 @@ +package model + +import "time" + +type User struct { + ID uint64 `db:"id" json:"-"` + Login string `db:"login" json:"login"` + Password string `db:"password" json:"-"` + Balance float64 `db:"balance"` + Withdraw float64 `db:"withdraw"` +} + +type Order struct { + Number string + Status string `json:"status"` + Accrual float64 `json:"accrual"` + UserLogin string `json:"-"` + Uploaded time.Time `db:"uploaded" json:"uploaded_at"` +} + +type Withdraw struct { + ID uint64 `db:"id" json:"-"` + Number string `db:"number" json:"order"` + Amount float64 `db:"amount" json:"sum"` + UserLogin string `db:"userlogin" json:"-"` + ProcessedAt time.Time `db:"processed" json:"processed_at"` +} diff --git a/internal/repository/postgres/user.go b/internal/repository/postgres/user.go new file mode 100644 index 0000000..9045c41 --- /dev/null +++ b/internal/repository/postgres/user.go @@ -0,0 +1,282 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/jmoiron/sqlx" + + "github.com/OlegVankov/fantastic-engine/internal/model" +) + +type UserRepository struct { + db *sqlx.DB +} + +func NewUserRepository(dsn string) *UserRepository { + db, _ := sqlx.Open("pgx", dsn) + repo := &UserRepository{ + db: db, + } + err := repo.Bootstrap(context.Background()) + if err != nil { + log.Fatal(err) + } + return repo +} + +func (r *UserRepository) Bootstrap(ctx context.Context) error { + users := `CREATE TABLE IF NOT EXISTS users +( + id bigserial PRIMARY KEY, + login varchar not null, + password varchar not null, + balance decimal(10, 2) default 0, + withdraw decimal(10, 2) default 0, + constraint users_unique_login unique (login) +); +` + orders := `CREATE TABLE IF NOT EXISTS orders +( + number varchar PRIMARY KEY not null, + status varchar, + accrual decimal(10, 2) default 0, + userlogin varchar not null, + uploaded timestamp with time zone not null default now(), + constraint orders_unique_number unique (number) +); +` + withdraw := `CREATE TABLE IF NOT EXISTS withdraw +( + id bigserial PRIMARY KEY, + number varchar not null, + amount decimal(10, 2) default 0, + userlogin varchar not null, + processed timestamp with time zone not null default now() +); +` + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + _, err = r.db.ExecContext(ctx, users) + if err != nil { + return err + } + + _, err = r.db.ExecContext(ctx, orders) + if err != nil { + return err + } + + _, err = r.db.ExecContext(ctx, withdraw) + if err != nil { + return err + } + + return tx.Commit() +} + +func (r *UserRepository) AddUser(ctx context.Context, login, password string) (*model.User, error) { + user := &model.User{} + err := r.db.QueryRowContext(ctx, + "insert into users(login, password) values($1, $2) returning *;", + login, + password, + ).Scan( + &user.ID, + &user.Login, + &user.Password, + &user.Balance, + &user.Withdraw, + ) + if err != nil { + return nil, err + } + return user, nil +} + +func (r *UserRepository) GetUser(ctx context.Context, login string) (*model.User, error) { + user := &model.User{} + err := r.db.QueryRowContext(ctx, + "select * from users where login = $1", + login, + ).Scan( + &user.ID, + &user.Login, + &user.Password, + &user.Balance, + &user.Withdraw, + ) + if err != nil { + return nil, err + } + return user, nil +} + +func (r *UserRepository) AddOrder(ctx context.Context, login, number string) (*model.Order, error) { + order := &model.Order{} + err := r.db.QueryRowContext(ctx, + "insert into orders(number, userlogin, status) values($1, $2, $3) returning *;", + number, + login, + "NEW", + ).Scan( + &order.Number, + &order.Status, + &order.Accrual, + &order.UserLogin, + &order.Uploaded, + ) + if err != nil { + return nil, err + } + return order, nil +} + +func (r *UserRepository) GetOrderByNumber(ctx context.Context, number string) (*model.Order, error) { + order := &model.Order{} + err := r.db.QueryRowContext(ctx, + "select number, userlogin from orders where number = $1", + number, + ).Scan( + &order.Number, + &order.UserLogin, + ) + if err != nil { + return nil, err + } + return order, nil +} + +func (r *UserRepository) UpdateOrder(ctx context.Context, number, status string, accrual float64) error { + order := &model.Order{} + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + err = tx.QueryRow("update orders set status = $1, accrual = $2 where number = $3 returning number, userlogin", + status, + accrual, + number, + ).Scan( + &order.Number, + &order.UserLogin, + ) + + if err != nil { + return err + } + + _, err = tx.Exec("update users set balance = balance + $1 where login = $2", + accrual, + order.UserLogin, + ) + + if err != nil { + return err + } + + return tx.Commit() +} + +func (r *UserRepository) GetOrdersByLogin(ctx context.Context, username string) ([]model.Order, error) { + orders := []model.Order{} + err := r.db.SelectContext(ctx, &orders, "select * from orders where userlogin = $1", username) + if err != nil { + return nil, err + } + return orders, nil +} + +func (r *UserRepository) GetBalance(ctx context.Context, username string) (*model.User, error) { + user := &model.User{} + err := r.db.QueryRowContext(ctx, + "select balance, withdraw from users where login = $1", + username, + ).Scan( + &user.Balance, + &user.Withdraw, + ) + if err != nil { + return nil, err + } + return user, nil +} + +func (r *UserRepository) GetOrders(ctx context.Context) ([]model.Order, error) { + orders := []model.Order{} + err := r.db.SelectContext(ctx, &orders, "select * from orders where status = 'NEW'") + if err != nil { + return nil, err + } + return orders, nil +} + +func (r *UserRepository) UpdateWithdraw(ctx context.Context, login, number string, sum float64) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + order := &model.Order{} + balance := 0.0 + tx.QueryRow("select balance from users where login = $1", login).Scan(&balance) + + if balance < sum { + return errors.New("balance error") + } + + _, err = tx.Exec("update users set balance = balance - $1, withdraw = withdraw + $1 where login = $2", + sum, + login, + ) + if err != nil { + return err + } + + err = tx.QueryRowContext(ctx, + "insert into orders(number, userlogin, status) values($1, $2, $3) returning *;", + number, + login, + "NEW", + ).Scan( + &order.Number, + &order.Status, + &order.Accrual, + &order.UserLogin, + &order.Uploaded, + ) + if err != nil { + return err + } + + _, err = tx.Exec("insert into withdraw(number, amount, userlogin) values($1, $2, $3);", + number, + sum, + login, + ) + if err != nil { + fmt.Println(err) + return err + } + + return tx.Commit() +} + +func (r *UserRepository) GetWithdrawals(ctx context.Context, login string) ([]model.Withdraw, error) { + withdrawals := []model.Withdraw{} + err := r.db.SelectContext(ctx, &withdrawals, "select * from withdraw where userlogin = $1 order by processed;", login) + if err != nil { + return nil, err + } + return withdrawals, nil +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..0062505 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,20 @@ +package repository + +import ( + "context" + + "github.com/OlegVankov/fantastic-engine/internal/model" +) + +type Repository interface { + AddUser(ctx context.Context, login, password string) (*model.User, error) + GetUser(ctx context.Context, login string) (*model.User, error) + AddOrder(ctx context.Context, login, number string) (*model.Order, error) + GetOrdersByLogin(ctx context.Context, number string) ([]model.Order, error) + GetOrders(ctx context.Context) ([]model.Order, error) + GetOrderByNumber(ctx context.Context, number string) (*model.Order, error) + UpdateOrder(ctx context.Context, number, status string, accrual float64) error + GetBalance(ctx context.Context, username string) (*model.User, error) + UpdateWithdraw(ctx context.Context, login, number string, sum float64) error + GetWithdrawals(ctx context.Context, login string) ([]model.Withdraw, error) +} diff --git a/internal/server.go b/internal/server.go new file mode 100644 index 0000000..7bba163 --- /dev/null +++ b/internal/server.go @@ -0,0 +1,81 @@ +package internal + +import ( + "context" + "errors" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/go-chi/chi/v5" + + "github.com/OlegVankov/fantastic-engine/internal/handler" + "github.com/OlegVankov/fantastic-engine/internal/handler/accrual" + "github.com/OlegVankov/fantastic-engine/internal/repository/postgres" +) + +type Server struct { + srv *http.Server + handler *handler.Handler +} + +func NewServer(addr, dsn string) *Server { + h := &handler.Handler{ + Repository: postgres.NewUserRepository(dsn), + } + + router := chi.NewRouter() + + router.Route("/api/user", func(r chi.Router) { + r.Post("/register", h.Register) + r.Post("/login", h.Login) + + r.Route("/", func(r chi.Router) { + r.Use(handler.Auth) + + r.Post("/orders", h.Orders) + r.Get("/orders", h.GetOrders) + + r.Post("/balance/withdraw", h.Withdraw) + r.Get("/balance", h.Balance) + r.Get("/withdrawals", h.Withdrawals) + }) + }) + + return &Server{ + srv: &http.Server{ + Addr: addr, + Handler: router, + MaxHeaderBytes: 1 << 20, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + }, + handler: h, + } +} + +func (s *Server) Run(accrualAddr string) { + go accrual.SendAccrual(accrualAddr, s.handler) + + go func() { + err := s.srv.ListenAndServe() + if !errors.Is(err, http.ErrServerClosed) { + log.Fatal("HTTP server ListenAndServe", err) + } + }() + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + sig := <-c + + log.Println("server", "Graceful shutdown starter with signal", sig.String()) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := s.srv.Shutdown(ctx); err != nil { + log.Fatal("server", err) + } + log.Println("server gracefully shutdown complete") +} diff --git a/internal/util/hash/hash.go b/internal/util/hash/hash.go new file mode 100644 index 0000000..8f3b28f --- /dev/null +++ b/internal/util/hash/hash.go @@ -0,0 +1,17 @@ +package hash + +import "golang.org/x/crypto/bcrypt" + +func StringToHash(password string) (string, error) { + bytePassword := []byte(password) + hash, err := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +func CheckPassword(hashPassword, password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashPassword), []byte(password)) + return err == nil +} diff --git a/internal/util/jwt/jwt.go b/internal/util/jwt/jwt.go new file mode 100644 index 0000000..977d4e6 --- /dev/null +++ b/internal/util/jwt/jwt.go @@ -0,0 +1,51 @@ +package jwt + +import ( + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +type UserClaim struct { + jwt.RegisteredClaims + Username string + UserID uint64 +} + +const secretKey = "AsDfGhJkL" + +func CreateToken(username string, userid uint64) (string, error) { + userClaim := &UserClaim{ + Username: username, + UserID: userid, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaim) + + tokenString, err := token.SignedString([]byte(secretKey)) + if err != nil { + return "", err + } + + return tokenString, nil +} + +func GetUser(tokenString string) string { + userClaim := &UserClaim{} + token, err := jwt.ParseWithClaims(tokenString, userClaim, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(secretKey), nil + }) + if err != nil { + return "" + } + if !token.Valid { + return "" + } + return userClaim.Username +} diff --git a/internal/util/lun/lun.go b/internal/util/lun/lun.go new file mode 100644 index 0000000..38c877e --- /dev/null +++ b/internal/util/lun/lun.go @@ -0,0 +1,21 @@ +package lun + +import "strconv" + +func CheckLun(num string) bool { + sum := 0 + parity := len(num) % 2 + + for i, v := range num { + digit, _ := strconv.Atoi(string(v)) + if i%2 == parity { + digit *= 2 + if digit > 9 { + digit = digit%10 + digit/10 + } + } + sum += digit + } + + return sum%10 == 0 +}