first commit

This commit is contained in:
GO 2021-09-03 09:52:42 +00:00
commit eef066b179
11 changed files with 477 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@ -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

17
Dockerfile Normal file
View File

@ -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

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# Product Catalog API
Blogs for Detailed Tutorial:
* <a href="https://learnai1.home.blog/2021/03/15/microservices-in-go/">Introductory Blog.</a>
* <a href="https://learnai1.home.blog/2021/03/18/microservices-in-go-part-2/">Adding More API's.</a>
* <a href="https://learnai1.home.blog/2021/06/27/authentication-in-go-microservices/">Basic Authentication in Microservices.</a>
* <a href="https://learnai1.home.blog/2021/07/08/microservices-in-go-part-iv-docker-and-go-microservices/">Docker and Go Microservices.</a>
* <a href="https://learnai1.home.blog/2021/08/05/https-server-in-go/">HTTPS Server in Go.</a>
## Run API
``` bash
go run .\main.go
```
Use the above command to run API on your local machine on port 9090.

30
data/data.json Normal file
View File

@ -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
}
]

123
entity/product.go Normal file
View File

@ -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:]...)
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/HelloWorld/goProductAPI
go 1.16
require github.com/gorilla/mux v1.8.0

2
go.sum Normal file
View File

@ -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=

166
handlers/handlers.go Normal file
View File

@ -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)
}
}

32
main.go Normal file
View File

@ -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())
}
}

22
server.crt Normal file
View File

@ -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-----

27
server.key Normal file
View File

@ -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-----