commit eef066b1796e651cc5690e95ccb07a30151ea9b3 Author: GO Date: Fri Sep 3 09:52:42 2021 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43cacbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ + +### VisualStudioCode ### +.vscode/* +!.vscode/tasks.json +!.vscode/launch.json +*.code-workspace + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c9ee3d0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.16.5 as builder + +WORKDIR /go-microservice/ + +COPY . . + +RUN CGO_ENABLED=0 go build -o microservice /go-microservice/main.go + +FROM alpine:latest + +WORKDIR /go-microservice + +COPY --from=builder /go-microservice/ /go-microservice/ + +EXPOSE 9090 + +CMD ./microservice \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d51710 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Product Catalog API + +Blogs for Detailed Tutorial: + +* Introductory Blog. +* Adding More API's. +* Basic Authentication in Microservices. +* Docker and Go Microservices. +* HTTPS Server in Go. +## Run API + +``` bash +go run .\main.go +``` + +Use the above command to run API on your local machine on port 9090. diff --git a/data/data.json b/data/data.json new file mode 100644 index 0000000..e836977 --- /dev/null +++ b/data/data.json @@ -0,0 +1,30 @@ +[ + { + "id": "e7f31219-7986-11eb-bd65-98fa9b64e75b", + "name": "iPhone 12 Pro", + "description": "Pro variant of iPhone 12", + "price": 100000, + "isAvailable": false + }, + { + "id": "f82fa5ec-7986-11eb-a8e3-98fa9b64e75b", + "name": "iPhone 12 Pro Max", + "description": "Pro max variant of iPhone 12", + "price": 120000, + "isAvailable": true + }, + { + "id": "e7f35219-7986-11eb-bd65-98fa9b64e75b", + "name": "iPhone 12 Mini", + "description": "Mini variant of iPhone 12", + "price": 60000, + "isAvailable": true + }, + { + "id": "e7f35219-8986-11eb-bd65-98fa9b64e75b", + "name": "iPhone 12", + "description": "Base variant of iPhone 12", + "price": 78000, + "isAvailable": true + } +] \ No newline at end of file diff --git a/entity/product.go b/entity/product.go new file mode 100644 index 0000000..3623ada --- /dev/null +++ b/entity/product.go @@ -0,0 +1,123 @@ +package entity + +import ( + "encoding/json" + "errors" + "io/ioutil" + "os" +) + +//Product defines a structure for an item in product catalog +type Product struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Price float64 `json:"price"` + IsAvailable bool `json:"isAvailable"` +} + +// ErrNoProduct is used if no product found +var ErrNoProduct = errors.New("no product found") + +// GetProducts returns the JSON file content if available else returns an error. +func GetProducts() ([]byte, error) { + // Read JSON file + data, err := ioutil.ReadFile("./data/data.json") + if err != nil { + return nil, err + } + return data, nil +} + +// GetProduct takes id as input and returns the corresponding product, else it returns ErrNoProduct error. +func GetProduct(id string) (Product, error) { + // Read JSON file + data, err := ioutil.ReadFile("./data/data.json") + if err != nil { + return Product{}, err + } + // read products + var products []Product + err = json.Unmarshal(data, &products) + if err != nil { + return Product{}, err + } + // iterate through product array + for i := 0; i < len(products); i++ { + // if we find one product with the given ID + if products[i].ID == id { + // return product + return products[i], nil + } + } + return Product{}, ErrNoProduct +} + +// DeleteProduct takes id as input and deletes the corresponding product, else it returns ErrNoProduct error. +func DeleteProduct(id string) error { + // Read JSON file + data, err := ioutil.ReadFile("./data/data.json") + if err != nil { + return err + } + // read products + var products []Product + err = json.Unmarshal(data, &products) + if err != nil { + return err + } + // iterate through product array + for i := 0; i < len(products); i++ { + // if we find one product with the given ID + if products[i].ID == id { + products = removeElement(products, i) + // Write Updated JSON file + updatedData, err := json.Marshal(products) + if err != nil { + return err + } + err = ioutil.WriteFile("./data/data.json", updatedData, os.ModePerm) + if err != nil { + return err + } + return nil + } + } + return ErrNoProduct +} + +// AddProduct adds an input product to the product list in JSON document. +func AddProduct(product Product) error { + // Load existing products and append the data to product list + var products []Product + data, err := ioutil.ReadFile("./data/data.json") + if err != nil { + return err + } + // Load our JSON file to memory using array of products + err = json.Unmarshal(data, &products) + if err != nil { + return err + } + // Add new Product to our list + products = append(products, product) + + // Write Updated JSON file + updatedData, err := json.Marshal(products) + if err != nil { + return err + } + err = ioutil.WriteFile("./data/data.json", updatedData, os.ModePerm) + if err != nil { + return err + } + + return nil +} + +// removeElement is used to remove element from product array at given index +func removeElement(arr []Product, index int) []Product { + ret := make([]Product, 0) + ret = append(ret, arr[:index]...) + return append(ret, arr[index+1:]...) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..20c6663 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/HelloWorld/goProductAPI + +go 1.16 + +require github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5350288 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..993c5c6 --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,166 @@ +package handlers + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "os" + + "github.com/HelloWorld/goProductAPI/entity" + "github.com/gorilla/mux" +) + +// GetProductsHandler is used to get data inside the products defined on our product catalog +func GetProductsHandler() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + data, err := entity.GetProducts() + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + // Write the body with JSON data + rw.Header().Add("content-type", "application/json") + rw.WriteHeader(http.StatusFound) + rw.Write(data) + } +} + +// GetProductHandler is used to get data inside the products defined on our product catalog +func GetProductHandler() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + // Read product ID + productID := mux.Vars(r)["id"] + product, err := entity.GetProduct(productID) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + responseData, err := json.Marshal(product) + if err != nil { + // Check if it is No product error or any other error + if errors.Is(err, entity.ErrNoProduct) { + // Write Header if no related product found. + rw.WriteHeader(http.StatusNoContent) + } else { + rw.WriteHeader(http.StatusInternalServerError) + } + return + } + // Write body with found product + rw.Header().Add("content-type", "application/json") + rw.WriteHeader(http.StatusFound) + rw.Write(responseData) + } +} + +// CreateProductHandler is used to create a new product and add to our product store. +func CreateProductHandler() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + // Read incoming JSON from request body + data, err := ioutil.ReadAll(r.Body) + // If no body is associated return with StatusBadRequest + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + // Check if data is proper JSON (data validation) + var product entity.Product + err = json.Unmarshal(data, &product) + if err != nil { + rw.WriteHeader(http.StatusExpectationFailed) + rw.Write([]byte("Invalid Data Format")) + return + } + err = entity.AddProduct(product) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + // return after writing Body + rw.WriteHeader(http.StatusCreated) + rw.Write([]byte("Added New Product")) + } +} + +// DeleteProductHandler deletes the product with given ID. +func DeleteProductHandler() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + // Read product ID + productID := mux.Vars(r)["id"] + err := entity.DeleteProduct(productID) + if err != nil { + // Check if it is No product error or any other error + if errors.Is(err, entity.ErrNoProduct) { + // Write Header if no related product found. + rw.WriteHeader(http.StatusNoContent) + } else { + rw.WriteHeader(http.StatusInternalServerError) + } + return + } + // Write Header with Accepted Status (done operation) + rw.WriteHeader(http.StatusAccepted) + } +} + +// UpdateProductHandler updates the product with given ID. +func UpdateProductHandler() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + // Read product ID + productID := mux.Vars(r)["id"] + err := entity.DeleteProduct(productID) + if err != nil { + if errors.Is(err, entity.ErrNoProduct) { + rw.WriteHeader(http.StatusNoContent) + } else { + rw.WriteHeader(http.StatusInternalServerError) + } + return + } + // Read incoming JSON from request body + data, err := ioutil.ReadAll(r.Body) + // If no body is associated return with StatusBadRequest + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + // Check if data is proper JSON (data validation) + var product entity.Product + err = json.Unmarshal(data, &product) + if err != nil { + rw.WriteHeader(http.StatusExpectationFailed) + rw.Write([]byte("Invalid Data Format")) + return + } + // Addproduct with the requested body + err = entity.AddProduct(product) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + // Write Header if no related product found. + rw.WriteHeader(http.StatusAccepted) + } +} + +func AuthHandler(h http.Handler) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if ok { + username := sha256.Sum256([]byte(os.Getenv("USER_NAME"))) + password := sha256.Sum256([]byte(os.Getenv("USER_PASS"))) + userHash := sha256.Sum256([]byte(user)) + passHash := sha256.Sum256([]byte(pass)) + validUser := subtle.ConstantTimeCompare(userHash[:],username[:]) == 1 + validPass := subtle.ConstantTimeCompare(passHash[:],password[:]) == 1 + if validPass && validUser{ + h.ServeHTTP(rw,r) + return + } + } + http.Error(rw, "No/Invalid Credentials", http.StatusUnauthorized) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..aafd809 --- /dev/null +++ b/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/HelloWorld/goProductAPI/handlers" + "github.com/gorilla/mux" +) + +func main() { + // Create new Router + router := mux.NewRouter() + // route properly to respective handlers + router.Handle("/products", handlers.GetProductsHandler()).Methods("GET") + router.Handle("/products", handlers.CreateProductHandler()).Methods("POST") + router.Handle("/products/{id}", handlers.GetProductHandler()).Methods("GET") + router.Handle("/products/{id}", handlers.DeleteProductHandler()).Methods("DELETE") + router.Handle("/products/{id}", handlers.UpdateProductHandler()).Methods("PUT") + + // Create new server and assign the router + server := http.Server{ + Addr: ":9090", + Handler: handlers.AuthHandler(router), + } + fmt.Println("Staring Product Catalog server on Port 9090") + // Start Server on defined port/host. + err := server.ListenAndServeTLS("server.crt", "server.key") + if err != nil { + fmt.Printf("Failed to start HTTPS server: %s", err.Error()) + } +} diff --git a/server.crt b/server.crt new file mode 100644 index 0000000..cdccdd9 --- /dev/null +++ b/server.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIUDQgu9+HrJrc7TEelsB1TYp+zhPkwDQYJKoZIhvcNAQEL +BQAwajELMAkGA1UEBhMCSU4xDjAMBgNVBAgMBURlbGhpMRIwEAYDVQQHDAlOZXcg +RGVsaGkxFDASBgNVBAoMC0hlbGxvIFdvcmxkMQ0wCwYDVQQLDARCbG9nMRIwEAYD +VQQDDAlsb2NhbGhvc3QwHhcNMjEwODA1MDMzNDQ0WhcNMzEwODAzMDMzNDQ0WjBq +MQswCQYDVQQGEwJJTjEOMAwGA1UECAwFRGVsaGkxEjAQBgNVBAcMCU5ldyBEZWxo +aTEUMBIGA1UECgwLSGVsbG8gV29ybGQxDTALBgNVBAsMBEJsb2cxEjAQBgNVBAMM +CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL8emPJ1 +tw0TrfJZQeLzHO2x0O36UetzeJKdf//IKt3P89B+2GGL6DzPdntTBM9J4PAzLzXe +uR8uL53XDWdfvhCd9oMYsgJ4L1XE6q+oZA6zWGC4voUwUOJgpJyXt8fdDdn1G6X/ +7Lyjw4u6KOkrFcwIPaM+MxgZGwICi1JDj7sYCOEw1gWsLrgD4XpN0Fky9QH1Uue+ +sxCZrrXFxiSXt0Xq51sT5BXrCkPhRrHUyhBay+VtVa+Lr2Yq+UfT3VUWERgcoJkx +YluYXMbV4YYZ53bbgrW+29UT+NKNKTF2dFJazZ4r5OzZAD3ihikvoCIZUaVtgOpf +fctWqG5bKmVpSaMCAwEAAaNTMFEwHQYDVR0OBBYEFPiCPV5yetrvDF5caB0t4XWx +S+NeMB8GA1UdIwQYMBaAFPiCPV5yetrvDF5caB0t4XWxS+NeMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBALiBoaqc9KxscNuU2mXTfXD4kv1bLy3C +zzGn3bBHMQuRfwkclkEZG0Cbmu+k30DtMhUt9DREM6fVhClUlR6my/0vC/EgUIm2 +yYOF/dBIySH/JkZz0qRst5NvicwPaqUQoyIIzk93Da8eE0uGHW7eV4tsAVkB0RHT +i4/EkGKZ/vJKyj/RI4zWvMCEFZcGtDtKf4rYzmcA3t0P1gbWt2+Gkhz1/+i52oIr +w2M2CQ9qpgv3jvwCfOpZVQDRMZnLVpP/s8KyOwnVu+n8c/z+zJe+U3x5Sl0eamDA +7CQ6RcorzWWzAhAQkrH0qWnF+BfF7GJ8VoLku0dnTzYsAQ5L4XaYf/I= +-----END CERTIFICATE----- diff --git a/server.key b/server.key new file mode 100644 index 0000000..6a09909 --- /dev/null +++ b/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAvx6Y8nW3DROt8llB4vMc7bHQ7fpR63N4kp1//8gq3c/z0H7Y +YYvoPM92e1MEz0ng8DMvNd65Hy4vndcNZ1++EJ32gxiyAngvVcTqr6hkDrNYYLi+ +hTBQ4mCknJe3x90N2fUbpf/svKPDi7oo6SsVzAg9oz4zGBkbAgKLUkOPuxgI4TDW +BawuuAPhek3QWTL1AfVS576zEJmutcXGJJe3RernWxPkFesKQ+FGsdTKEFrL5W1V +r4uvZir5R9PdVRYRGBygmTFiW5hcxtXhhhnndtuCtb7b1RP40o0pMXZ0UlrNnivk +7NkAPeKGKS+gIhlRpW2A6l99y1aoblsqZWlJowIDAQABAoIBAQClfS0a5Ws3246H +h1pR1gl6mLo9Fr/QjRAehFrNdNoJb4PDSdK7xJW38jy51M0ZYPNxiiCbGNxbb3az +yf9FP9YoNV+7bKrXEJKMRhKhP8JEKG+icNYoJgoju2NOZOEyIutXi7IBL3Yicftl +BjFelXwuTARzUeyUNUj5mJJjDTVr3oi7A/HyoEn66PmWSGgNB618tYxA55PwRgJO +hd+OakpW90kLb3RsySts9cpIh1eEJLTKafTR4uY7SrAvMjFa9Zn4MpRYXMEq43DC +382dlqYBPXMPN4YJEKZ5CmxFMaZ3a1Nr0G60RyQqwmB/fRWg+OZUOjjFOVllnOOY +5jnSIy9BAoGBAONwsIpaze2TeM8Ij4NxBqwRAh/2zb5t0okD/zbhf2sPyrEP8+hI +egG10kQh7t0MhRklSOmv3NfDxr998XfBjswXdnsoIvP+BmlLG01zRh/49cdBnOty +Za/zu5msyYspopPrDjwF+RtMHOXpXNuzFhxMYWdfOBO7/lOSka8ABFqZAoGBANce +VzQW6OFLAE5fwwkf5W4cjOGaSpkKq6KwBkQayvRrGQMf3pu7Q2pdlcoJIhGOy8Bc +N2iI7qE4P52MmAg51TyIDcrhTrMLkoMXLmeyBC1M6JT6t1yGTpPkf6ZdVFAZZEAp +3l2dv8pyPRpr5juE8YcabgF5deo3zxieB6bYlkebAoGAS63goHjsksQCa+luT49Z +aAHU0iv+dAH5DyxsTKemDUrY6CflwgHzzwPgLlmYMKeM1jwo0dF5y7XSOT/ADFg0 +msan3v0Q/F0nZvvd3tyfld3yclXr0BBls7GHV/A9s/erqEqLlv9pz2J5LyuCgXxK +vCnSM2Jkt3RTgR2BKlj4GekCgYAxUxilrfcZ6WuZjOWYiwK9W7iF5i3ip4qxU/Er +3oTYxFHI4J7XUHnlwq2c1LlGE1rusXZW9sbYmqAjjOAzSqd1KLEY6s5zyVx/yGnw +huXkSTUvK8mtYnJUANmwGMhDUX8mIzOEfa5DSixuiX0R+qqy0sGUfvgli0RmHZ4d +iJ30rwKBgDuW1L0DJnH2n8e3uhJF8W1rKDynQdzGdPc4Bl8oSfvDMpIGz1cTecKV ++30AWz1ui5u1qlg7gsjnt6Z7g3ztXfHNP1gHws5lC8YsjAEOyZ2f6JHOGAmHOsRb +G1T9WS4wq4N1DJVJ1aQcgvF1BuejzUwyWTABgpbSQzCIaUXAeX9I +-----END RSA PRIVATE KEY-----