├── .gitignore ├── rest.png ├── go.mod ├── routes └── routes.go ├── docker-compose.yaml ├── middleware └── middleware.go ├── database ├── databasetup.go └── cart.go ├── main.go ├── models └── models.go ├── tokens └── tokengen.go ├── controllers ├── address.go ├── cart.go └── controllers.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5olitude/ecommerce/HEAD/rest.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ecommerce 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-gonic/gin v1.7.4 8 | github.com/go-playground/validator/v10 v10.9.0 9 | go.mongodb.org/mongo-driver v1.7.2 10 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 11 | ) 12 | -------------------------------------------------------------------------------- /routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "ecommerce/controllers" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func UserRoutes(incomingRoutes *gin.Engine) { 10 | incomingRoutes.POST("/users/signup", controllers.SignUp()) 11 | incomingRoutes.POST("/users/login", controllers.Login()) 12 | incomingRoutes.POST("/admin/addproduct", controllers.ProductViewerAdmin()) 13 | incomingRoutes.GET("/users/productview", controllers.SearchProduct()) 14 | incomingRoutes.GET("/users/search", controllers.SearchProductByQuery()) 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | mongo: 6 | image: mongo:5.0.3 7 | ports: 8 | - 27017:27017 9 | environment: 10 | MONGO_INITDB_ROOT_USERNAME: development 11 | MONGO_INITDB_ROOT_PASSWORD: testpassword 12 | 13 | mongo-express: 14 | image: mongo-express 15 | ports: 16 | - 8081:8081 17 | environment: 18 | ME_CONFIG_MONGODB_ADMINUSERNAME: development 19 | ME_CONFIG_MONGODB_ADMINPASSWORD: testpassword 20 | ME_CONFIG_MONGODB_URL: mongodb://development:testpassword@mongo:27017/ 21 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | token "ecommerce/tokens" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func Authentication() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | ClientToken := c.Request.Header.Get("token") 13 | if ClientToken == "" { 14 | c.JSON(http.StatusInternalServerError, gin.H{"error": "No Authorization Header Provided"}) 15 | c.Abort() 16 | return 17 | } 18 | claims, err := token.ValidateToken(ClientToken) 19 | if err != "" { 20 | c.JSON(http.StatusInternalServerError, gin.H{"error": err}) 21 | c.Abort() 22 | return 23 | } 24 | // Why do you add so many claims to your JWT token? 25 | // Is just the email or uid (which I assume are both unique) and fetch the user if you need to get more data from the user? 26 | c.Set("email", claims.Email) 27 | c.Set("uid", claims.Uid) 28 | c.Next() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/databasetup.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | // 4 | import ( 5 | "context" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | "go.mongodb.org/mongo-driver/mongo" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | ) 13 | 14 | // DBSet is more Go-like compared to DBSet. Take a look at https://pkg.go.dev/database/sql@go1.17.2#OpenDB for example. Abbreviations are written in allcaps in Go. Examples: http.ServeTLS(), time.UTC 15 | func DBSet() *mongo.Client { 16 | client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://development:testpassword@localhost:27017")) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 21 | defer cancel() 22 | err = client.Connect(ctx) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | err = client.Ping(context.TODO(), nil) 28 | if err != nil { 29 | log.Println("failed to connect to mongodb") 30 | return nil 31 | } 32 | 33 | // This doesn't work, even if you don't have mongodb running this line still prints. 34 | fmt.Println("Successfully Connected to the mongodb") 35 | return client 36 | } 37 | 38 | // This is a global var, don't use global vars for database connections. 39 | // Instead use dependency injection to give your HTTP handlers access to 40 | // the mongodb connection pool. 41 | var Client *mongo.Client = DBSet() 42 | 43 | func UserData(client *mongo.Client, CollectionName string) *mongo.Collection { 44 | var collection *mongo.Collection = client.Database("Ecommerce").Collection(CollectionName) 45 | return collection 46 | 47 | } 48 | 49 | func ProductData(client *mongo.Client, CollectionName string) *mongo.Collection { 50 | var productcollection *mongo.Collection = client.Database("Ecommerce").Collection(CollectionName) 51 | return productcollection 52 | } 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "ecommerce/controllers" 5 | "ecommerce/database" 6 | "ecommerce/middleware" 7 | "ecommerce/routes" 8 | "log" 9 | "os" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | func main() { 15 | // This is good and follows the advice of: https://12factor.net/config. 16 | // But you should do this for all config: mongodb (credentials, database, collections), SECRET_KEY in tokengen.go. 17 | port := os.Getenv("PORT") 18 | if port == "" { 19 | port = "8000" 20 | } 21 | 22 | // This is still a bad way of dependency injection because I would break a 23 | // lot of your code if I would do it properly. You want to create your 24 | // database connection in your main.go file and give the database client 25 | // to the database.ProductData and database.UserData functions. 26 | app := controllers.NewApplication(database.ProductData(database.Client, "Products"), database.UserData(database.Client, "Users")) 27 | 28 | router := gin.New() 29 | router.Use(gin.Logger()) 30 | routes.UserRoutes(router) 31 | // The authentication middleware is applied to all routes, including the /users/signup route. So nobody can actually use the application. 32 | router.Use(middleware.Authentication()) 33 | // Your routes are inconsistent starting with and without '/'. 34 | router.GET("/addtocart", app.AddToCart()) 35 | router.GET("/removeitem", app.RemoveItem()) 36 | router.GET("/listcart", controllers.GetItemFromCart()) 37 | router.POST("/addaddress", controllers.AddAddress()) 38 | router.PUT("/edithomeaddress", controllers.EditHomeAddress()) 39 | router.PUT("/editworkaddress", controllers.EditWorkAddress()) 40 | router.GET("/deleteaddresses", controllers.DeleteAddress()) 41 | router.GET("/cartcheckout", app.BuyFromCart()) 42 | router.GET("/instantbuy", app.InstantBuy()) 43 | //router.GET("logout", controllers.Logout()) 44 | //break :) 45 | 46 | // Log the error that the router can possibly return. 47 | log.Fatal(router.Run(":" + port)) 48 | } 49 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // User represents the users collection in MongoDB. 10 | type User struct { 11 | ID primitive.ObjectID `json:"_id" bson:"_id"` 12 | First_Name *string `json:"first_name" validate:"required,min=2,max=30"` 13 | Last_Name *string `json:"last_name" validate:"required,min=2,max=30"` 14 | Password *string `json:"password" validate:"required,min=6"` 15 | Email *string `json:"email" validate:"email,required"` 16 | Phone *string `json:"phone" validate:"required"` 17 | Token *string `json:"token"` 18 | Refresh_Token *string `josn:"refresh_token"` 19 | Created_At time.Time `json:"created_at"` 20 | Updated_At time.Time `json:"updtaed_at"` 21 | User_ID string `json:"user_id"` 22 | UserCart []ProductUser `json:"usercart" bson:"usercart"` 23 | Address_Details []Address `json:"address" bson:"address"` 24 | Order_Status []Order `json:"orders" bson:"orders"` 25 | } 26 | 27 | type Product struct { 28 | Product_ID primitive.ObjectID `bson:"_id"` 29 | Product_Name *string `json:"product_name"` 30 | Price *uint64 `json:"price"` 31 | Rating *uint8 `json:"rating"` 32 | Image *string `json:"image"` 33 | } 34 | 35 | type ProductUser struct { 36 | Product_ID primitive.ObjectID `bson:"_id"` 37 | Product_Name *string `json:"product_name" bson:"product_name"` 38 | Price int `json:"price" bson:"price"` 39 | Rating *uint `json:"rating" bson:"rating"` 40 | Image *string `json:"image" bson:"image"` 41 | } 42 | 43 | type Address struct { 44 | Address_id primitive.ObjectID `bson:"_id"` 45 | House *string `json:"house_name" bson:"house_name"` 46 | Street *string `json:"street_name" bson:"street_name"` 47 | City *string `json:"city_name" bson:"city_name"` 48 | Pincode *string `json:"pin_code" bson:"pin_code"` 49 | } 50 | 51 | type Order struct { 52 | Order_ID primitive.ObjectID `bson:"_id"` 53 | Order_Cart []ProductUser `json:"order_list" bson:"order_list"` 54 | Orderered_At time.Time `json:"ordered_on" bson:"ordered_on"` 55 | Price int `json:"total_price" bson:"total_price"` 56 | Discount *int `json:"discount" bson:"discount"` 57 | Payment_Method Payment `json:"payment_method" bson:"payment_method"` 58 | } 59 | 60 | type Payment struct { 61 | Digital bool `json:"digital" bson:"digital"` 62 | COD bool `json:"cod" bson:"cod"` 63 | } 64 | -------------------------------------------------------------------------------- /tokens/tokengen.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "context" 5 | "ecommerce/database" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | jwt "github.com/dgrijalva/jwt-go" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/bson/primitive" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | "go.mongodb.org/mongo-driver/mongo/options" 15 | ) 16 | 17 | type SignedDetails struct { 18 | Email string 19 | First_Name string 20 | Last_Name string 21 | Uid string 22 | jwt.StandardClaims 23 | } 24 | 25 | var UserData *mongo.Collection = database.UserData(database.Client, "Users") 26 | var SECRET_KEY = os.Getenv("SECRET_LOVE") 27 | 28 | func TokenGenerator(email string, firstname string, lastname string, uid string) (signedtoken string, signedrefreshtoken string, err error) { 29 | claims := &SignedDetails{ 30 | Email: email, 31 | First_Name: firstname, 32 | Last_Name: lastname, 33 | Uid: uid, 34 | StandardClaims: jwt.StandardClaims{ 35 | ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(24)).Unix(), 36 | }, 37 | } 38 | refreshclaims := &SignedDetails{ 39 | StandardClaims: jwt.StandardClaims{ 40 | ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(168)).Unix(), 41 | }, 42 | } 43 | token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(SECRET_KEY)) 44 | if err != nil { 45 | return "", "", err 46 | } 47 | refreshtoken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshclaims).SignedString([]byte(SECRET_KEY)) 48 | if err != nil { 49 | // Try to reduce your panic and return an error to the user instead so your application stays running. 50 | // It looks like Gin doesn't recover from panic's automatically so your website will crash if jwt.NewWithClaims returns an error. 51 | log.Panicln(err) 52 | return 53 | } 54 | return token, refreshtoken, err 55 | } 56 | 57 | func ValidateToken(signedtoken string) (claims *SignedDetails, msg string) { 58 | token, err := jwt.ParseWithClaims(signedtoken, &SignedDetails{}, func(token *jwt.Token) (interface{}, error) { 59 | return []byte(SECRET_KEY), nil 60 | }) 61 | 62 | if err != nil { 63 | msg = err.Error() 64 | return 65 | } 66 | claims, ok := token.Claims.(*SignedDetails) 67 | if !ok { 68 | msg = "The Token is invalid" 69 | return 70 | } 71 | if claims.ExpiresAt < time.Now().Local().Unix() { 72 | msg = "token is expired" 73 | return 74 | } 75 | return claims, msg 76 | } 77 | 78 | func UpdateAllTokens(signedtoken string, signedrefreshtoken string, userid string) { 79 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 80 | var updateobj primitive.D 81 | updateobj = append(updateobj, bson.E{Key: "token", Value: signedtoken}) 82 | updateobj = append(updateobj, bson.E{Key: "refresh_token", Value: signedrefreshtoken}) 83 | updated_at, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) 84 | updateobj = append(updateobj, bson.E{Key: "updatedat", Value: updated_at}) 85 | upsert := true 86 | filter := bson.M{"user_id": userid} 87 | opt := options.UpdateOptions{ 88 | Upsert: &upsert, 89 | } 90 | _, err := UserData.UpdateOne(ctx, filter, bson.D{ 91 | {Key: "$set", Value: updateobj}, 92 | }, 93 | &opt) 94 | defer cancel() 95 | if err != nil { 96 | log.Panic(err) 97 | return 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /database/cart.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "ecommerce/models" 6 | "errors" 7 | "log" 8 | "time" 9 | 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | ) 14 | 15 | var ( 16 | // We give the user the same error 17 | ErrCantFindProduct = errors.New("can't find product") 18 | ErrCantDecodeProducts = errors.New("can't find product") 19 | ErrUserIDIsNotValid = errors.New("user is not valid") 20 | ErrCantUpdateUser = errors.New("cannot add product to cart") 21 | ErrCantRemoveItem = errors.New("cannot remove item from cart") 22 | ErrCantGetItem = errors.New("cannot get item from cart ") 23 | ErrCantBuyCartItem = errors.New("cannot update the purchase") 24 | ) 25 | 26 | func AddProductToCart(ctx context.Context, prodCollection, userCollection *mongo.Collection, productID primitive.ObjectID, userID string) error { 27 | searchfromdb, err := prodCollection.Find(ctx, bson.M{"_id": productID}) 28 | if err != nil { 29 | log.Println(err) 30 | return ErrCantFindProduct 31 | } 32 | var productcart []models.ProductUser 33 | err = searchfromdb.All(ctx, &productcart) 34 | if err != nil { 35 | log.Println(err) 36 | return ErrCantDecodeProducts 37 | } 38 | 39 | id, err := primitive.ObjectIDFromHex(userID) 40 | if err != nil { 41 | log.Println(err) 42 | return ErrUserIDIsNotValid 43 | } 44 | 45 | filter := bson.D{primitive.E{Key: "_id", Value: id}} 46 | update := bson.D{{Key: "$push", Value: bson.D{primitive.E{Key: "usercart", Value: bson.D{{Key: "$each", Value: productcart}}}}}} 47 | _, err = userCollection.UpdateOne(ctx, filter, update) 48 | if err != nil { 49 | return ErrCantUpdateUser 50 | } 51 | return nil 52 | } 53 | 54 | func RemoveCartItem(ctx context.Context, prodCollection, userCollection *mongo.Collection, productID primitive.ObjectID, userID string) error { 55 | id, err := primitive.ObjectIDFromHex(userID) 56 | if err != nil { 57 | log.Println(err) 58 | return ErrUserIDIsNotValid 59 | } 60 | filter := bson.D{primitive.E{Key: "_id", Value: id}} 61 | update := bson.M{"$pull": bson.M{"usercart": bson.M{"_id": productID}}} 62 | _, err = userCollection.UpdateMany(ctx, filter, update) 63 | if err != nil { 64 | return ErrCantRemoveItem 65 | } 66 | return nil 67 | 68 | } 69 | 70 | func BuyItemFromCart(ctx context.Context, userCollection *mongo.Collection, userID string) error { 71 | id, err := primitive.ObjectIDFromHex(userID) 72 | if err != nil { 73 | log.Println(err) 74 | return ErrUserIDIsNotValid 75 | } 76 | var getcartitems models.User 77 | var ordercart models.Order 78 | ordercart.Order_ID = primitive.NewObjectID() 79 | ordercart.Orderered_At = time.Now() 80 | ordercart.Order_Cart = make([]models.ProductUser, 0) 81 | ordercart.Payment_Method.COD = true 82 | unwind := bson.D{{Key: "$unwind", Value: bson.D{primitive.E{Key: "path", Value: "$usercart"}}}} 83 | grouping := bson.D{{Key: "$group", Value: bson.D{primitive.E{Key: "_id", Value: "$_id"}, {Key: "total", Value: bson.D{primitive.E{Key: "$sum", Value: "$usercart.price"}}}}}} 84 | currentresults, err := userCollection.Aggregate(ctx, mongo.Pipeline{unwind, grouping}) 85 | ctx.Done() 86 | if err != nil { 87 | panic(err) 88 | } 89 | var getusercart []bson.M 90 | if err = currentresults.All(ctx, &getusercart); err != nil { 91 | panic(err) 92 | } 93 | var total_price int32 94 | for _, user_item := range getusercart { 95 | price := user_item["total"] 96 | total_price = price.(int32) 97 | } 98 | ordercart.Price = int(total_price) 99 | filter := bson.D{primitive.E{Key: "_id", Value: id}} 100 | update := bson.D{{Key: "$push", Value: bson.D{primitive.E{Key: "orders", Value: ordercart}}}} 101 | _, err = userCollection.UpdateMany(ctx, filter, update) 102 | if err != nil { 103 | log.Println(err) 104 | } 105 | err = userCollection.FindOne(ctx, bson.D{primitive.E{Key: "_id", Value: id}}).Decode(&getcartitems) 106 | if err != nil { 107 | log.Println(err) 108 | } 109 | filter2 := bson.D{primitive.E{Key: "_id", Value: id}} 110 | update2 := bson.M{"$push": bson.M{"orders.$[].order_list": bson.M{"$each": getcartitems.UserCart}}} 111 | _, err = userCollection.UpdateOne(ctx, filter2, update2) 112 | if err != nil { 113 | log.Println(err) 114 | } 115 | usercart_empty := make([]models.ProductUser, 0) 116 | filtered := bson.D{primitive.E{Key: "_id", Value: id}} 117 | updated := bson.D{{Key: "$set", Value: bson.D{primitive.E{Key: "usercart", Value: usercart_empty}}}} 118 | _, err = userCollection.UpdateOne(ctx, filtered, updated) 119 | if err != nil { 120 | return ErrCantBuyCartItem 121 | 122 | } 123 | return nil 124 | } 125 | 126 | func InstantBuyer(ctx context.Context, prodCollection, userCollection *mongo.Collection, productID primitive.ObjectID, UserID string) error { 127 | id, err := primitive.ObjectIDFromHex(UserID) 128 | if err != nil { 129 | log.Println(err) 130 | return ErrUserIDIsNotValid 131 | } 132 | var product_details models.ProductUser 133 | var orders_detail models.Order 134 | orders_detail.Order_ID = primitive.NewObjectID() 135 | orders_detail.Orderered_At = time.Now() 136 | orders_detail.Order_Cart = make([]models.ProductUser, 0) 137 | orders_detail.Payment_Method.COD = true 138 | err = prodCollection.FindOne(ctx, bson.D{primitive.E{Key: "_id", Value: productID}}).Decode(&product_details) 139 | if err != nil { 140 | log.Println(err) 141 | } 142 | orders_detail.Price = product_details.Price 143 | filter := bson.D{primitive.E{Key: "_id", Value: id}} 144 | update := bson.D{{Key: "$push", Value: bson.D{primitive.E{Key: "orders", Value: orders_detail}}}} 145 | _, err = userCollection.UpdateOne(ctx, filter, update) 146 | if err != nil { 147 | log.Println(err) 148 | } 149 | filter2 := bson.D{primitive.E{Key: "_id", Value: id}} 150 | update2 := bson.M{"$push": bson.M{"orders.$[].order_list": product_details}} 151 | _, err = userCollection.UpdateOne(ctx, filter2, update2) 152 | if err != nil { 153 | log.Println(err) 154 | } 155 | return nil 156 | } 157 | -------------------------------------------------------------------------------- /controllers/address.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "ecommerce/models" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/bson/primitive" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | ) 15 | 16 | //function to add the address and limited to 2 17 | //home and work address 18 | /* 19 | { 20 | "house_name":"jupyterlab", 21 | "street_name":"notebook", 22 | "city_name":"josua", 23 | "pin_code":"685607" 24 | } 25 | The Post Request Url will look like this 26 | POST 27 | http://localhost:8000/addadress?id=user_id************* 28 | 29 | */ 30 | func AddAddress() gin.HandlerFunc { 31 | return func(c *gin.Context) { 32 | user_id := c.Query("id") 33 | if user_id == "" { 34 | c.Header("Content-Type", "application/json") 35 | c.JSON(http.StatusNotFound, gin.H{"error": "Invalid code"}) 36 | c.Abort() 37 | return 38 | } 39 | address, err := primitive.ObjectIDFromHex(user_id) 40 | if err != nil { 41 | c.IndentedJSON(500, "Internal Server Error") 42 | } 43 | var addresses models.Address 44 | addresses.Address_id = primitive.NewObjectID() 45 | if err = c.BindJSON(&addresses); err != nil { 46 | c.IndentedJSON(http.StatusNotAcceptable, err.Error()) 47 | } 48 | 49 | // What is the point of these massive timeouts? 100 seconds! 50 | // I would place the context at the start of the handler and maybe give 51 | // it 10 seconds maximum. Most people already stop ordering 52 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 53 | 54 | match_filter := bson.D{{Key: "$match", Value: bson.D{primitive.E{Key: "_id", Value: address}}}} 55 | unwind := bson.D{{Key: "$unwind", Value: bson.D{primitive.E{Key: "path", Value: "$address"}}}} 56 | group := bson.D{{Key: "$group", Value: bson.D{primitive.E{Key: "_id", Value: "$address_id"}, {Key: "count", Value: bson.D{primitive.E{Key: "$sum", Value: 1}}}}}} 57 | 58 | pointcursor, err := UserCollection.Aggregate(ctx, mongo.Pipeline{match_filter, unwind, group}) 59 | if err != nil { 60 | c.IndentedJSON(500, "Internal Server Error") 61 | } 62 | 63 | var addressinfo []bson.M 64 | if err = pointcursor.All(ctx, &addressinfo); err != nil { 65 | panic(err) 66 | } 67 | 68 | var size int32 69 | for _, address_no := range addressinfo { 70 | count := address_no["count"] 71 | size = count.(int32) 72 | } 73 | if size < 2 { 74 | filter := bson.D{primitive.E{Key: "_id", Value: address}} 75 | update := bson.D{{Key: "$push", Value: bson.D{primitive.E{Key: "address", Value: addresses}}}} 76 | _, err := UserCollection.UpdateOne(ctx, filter, update) 77 | if err != nil { 78 | fmt.Println(err) 79 | } 80 | } else { 81 | c.IndentedJSON(400, "Not Allowed ") 82 | } 83 | defer cancel() 84 | ctx.Done() 85 | } 86 | } 87 | 88 | //function to edit the address put request 89 | /* 90 | 91 | { 92 | "house_name":"jupyterlab", 93 | "street_name":"notebook", 94 | "city_name":"mars", 95 | "pin_code":"12231997" 96 | } 97 | PUT 98 | http://localhost:8000/edithomeaddress?id=xxxxxxxxxxxxxxxxxxxxxxxxx 99 | 100 | */ 101 | func EditHomeAddress() gin.HandlerFunc { 102 | return func(c *gin.Context) { 103 | user_id := c.Query("id") 104 | if user_id == "" { 105 | c.Header("Content-Type", "application/json") 106 | c.JSON(http.StatusNotFound, gin.H{"Error": "Invalid"}) 107 | c.Abort() 108 | return 109 | } 110 | usert_id, err := primitive.ObjectIDFromHex(user_id) 111 | if err != nil { 112 | c.IndentedJSON(500, err) 113 | } 114 | var editaddress models.Address 115 | if err := c.BindJSON(&editaddress); err != nil { 116 | c.IndentedJSON(http.StatusBadRequest, err.Error()) 117 | } 118 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 119 | defer cancel() 120 | filter := bson.D{primitive.E{Key: "_id", Value: usert_id}} 121 | update := bson.D{{Key: "$set", Value: bson.D{primitive.E{Key: "address.0.house_name", Value: editaddress.House}, {Key: "address.0.street_name", Value: editaddress.Street}, {Key: "address.0.city_name", Value: editaddress.City}, {Key: "address.0.pin_code", Value: editaddress.Pincode}}}} 122 | _, err = UserCollection.UpdateOne(ctx, filter, update) 123 | if err != nil { 124 | c.IndentedJSON(500, "Something Went Wrong") 125 | return 126 | } 127 | defer cancel() 128 | ctx.Done() 129 | c.IndentedJSON(200, "Successfully Updated the Home address") 130 | } 131 | } 132 | 133 | //function to edit the work address put request 134 | /* 135 | 136 | { 137 | "house_name":"jupyterlab", 138 | "street_name":"notebook", 139 | "city_name":"mars", 140 | "pin_code":"12231997" 141 | } 142 | PUT 143 | http://localhost:8000/editworkaddress?id=xxxxxxxxxxxxxxxxxxxxxxxxx 144 | 145 | */ 146 | 147 | func EditWorkAddress() gin.HandlerFunc { 148 | return func(c *gin.Context) { 149 | user_id := c.Query("id") 150 | if user_id == "" { 151 | c.Header("Content-Type", "application/json") 152 | c.JSON(http.StatusNotFound, gin.H{"Error": "Wrong id not provided"}) 153 | c.Abort() 154 | return 155 | } 156 | usert_id, err := primitive.ObjectIDFromHex(user_id) 157 | if err != nil { 158 | c.IndentedJSON(500, err) 159 | } 160 | var editaddress models.Address 161 | if err := c.BindJSON(&editaddress); err != nil { 162 | c.IndentedJSON(http.StatusBadRequest, err.Error()) 163 | } 164 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 165 | defer cancel() 166 | filter := bson.D{primitive.E{Key: "_id", Value: usert_id}} 167 | update := bson.D{{Key: "$set", Value: bson.D{primitive.E{Key: "address.1.house_name", Value: editaddress.House}, {Key: "address.1.street_name", Value: editaddress.Street}, {Key: "address.1.city_name", Value: editaddress.City}, {Key: "address.1.pin_code", Value: editaddress.Pincode}}}} 168 | _, err = UserCollection.UpdateOne(ctx, filter, update) 169 | if err != nil { 170 | c.IndentedJSON(500, "something Went wrong") 171 | return 172 | } 173 | defer cancel() 174 | ctx.Done() 175 | c.IndentedJSON(200, "Successfully updated the Work Address") 176 | } 177 | } 178 | 179 | /********************************************************************************************/ 180 | 181 | //function to delete the address here both the address will be removed fix soon 182 | //GET request 183 | //http://localhost:8000/deleteaddresses?id=xxxxxxxxxxxxxxxxxxxxxxxx 184 | 185 | func DeleteAddress() gin.HandlerFunc { 186 | return func(c *gin.Context) { 187 | user_id := c.Query("id") 188 | if user_id == "" { 189 | c.Header("Content-Type", "application/json") 190 | c.JSON(http.StatusNotFound, gin.H{"Error": "Invalid Search Index"}) 191 | c.Abort() 192 | return 193 | } 194 | addresses := make([]models.Address, 0) 195 | usert_id, err := primitive.ObjectIDFromHex(user_id) 196 | if err != nil { 197 | c.IndentedJSON(500, "Internal Server Error") 198 | } 199 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 200 | defer cancel() 201 | filter := bson.D{primitive.E{Key: "_id", Value: usert_id}} 202 | update := bson.D{{Key: "$set", Value: bson.D{primitive.E{Key: "address", Value: addresses}}}} 203 | _, err = UserCollection.UpdateOne(ctx, filter, update) 204 | if err != nil { 205 | c.IndentedJSON(404, "Wromg") 206 | return 207 | } 208 | defer cancel() 209 | ctx.Done() 210 | c.IndentedJSON(200, "Successfully Deleted!") 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /controllers/cart.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "ecommerce/database" 6 | "ecommerce/models" 7 | "errors" 8 | "log" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/bson/primitive" 15 | "go.mongodb.org/mongo-driver/mongo" 16 | ) 17 | 18 | type Application struct { 19 | prodCollection *mongo.Collection 20 | userCollection *mongo.Collection 21 | } 22 | 23 | func NewApplication(prodCollection, userCollection *mongo.Collection) *Application { 24 | return &Application{ 25 | prodCollection: prodCollection, 26 | userCollection: userCollection, 27 | } 28 | } 29 | 30 | // AddToCart adds products to the cart of the user. 31 | // GET request 32 | // http://localhost:8000/addtocart?id=xxxproduct_id&normal=xxxxxxuser_idxxxxxx 33 | // I add dependency to this handler to show you how much simpler your handler 34 | // become. Your handlers should validate input call one or two functions. 35 | // These business logic functions return an answer and probably an error. 36 | // Your handler should then handle the error and construct a repsonse that 37 | // matches the output and error. 38 | func (app *Application) AddToCart() gin.HandlerFunc { 39 | return func(c *gin.Context) { 40 | // Validate the input of the user 41 | productQueryID := c.Query("id") 42 | if productQueryID == "" { 43 | log.Println("product id is empty") 44 | // Gin has this c.Errors that is supposed to be used to catch 45 | // all errors that are generated in your handler. I don't understand 46 | // why you would use that so I'm ignoring it here. 47 | _ = c.AbortWithError(http.StatusBadRequest, errors.New("product id is empty")) 48 | return 49 | } 50 | 51 | // Why is this called normal and not userID? 52 | userQueryID := c.Query("userID") 53 | if userQueryID == "" { 54 | log.Println("user id is empty") 55 | _ = c.AbortWithError(http.StatusBadRequest, errors.New("user id is empty")) 56 | return 57 | } 58 | // Don't ignore errors! 59 | productID, err := primitive.ObjectIDFromHex(productQueryID) 60 | if err != nil { 61 | log.Println(err) 62 | c.AbortWithStatus(http.StatusInternalServerError) 63 | return 64 | } 65 | 66 | // Do your database queries 67 | var ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) 68 | // You have sometimes multiple defer cancel() in your functions. 69 | defer cancel() 70 | 71 | err = database.AddProductToCart(ctx, app.prodCollection, app.userCollection, productID, userQueryID) 72 | if err != nil { 73 | // This error is actually controlled by us so we don't leak any 74 | // sensitive information about our mongodb server or what went wrong. 75 | c.IndentedJSON(http.StatusInternalServerError, err) 76 | } 77 | c.IndentedJSON(200, "Successfully Added to the cart") 78 | } 79 | } 80 | 81 | //function to remove item from cart 82 | //GET Request 83 | //http://localhost:8000/addtocart?id=xxxproduct_id&normal=xxxxxxuser_idxxxxxx 84 | func (app *Application) RemoveItem() gin.HandlerFunc { 85 | return func(c *gin.Context) { 86 | productQueryID := c.Query("id") 87 | if productQueryID == "" { 88 | log.Println("product id is inavalid") 89 | _ = c.AbortWithError(http.StatusBadRequest, errors.New("product id is empty")) 90 | return 91 | } 92 | 93 | userQueryID := c.Query("userID") 94 | if userQueryID == "" { 95 | log.Println("user id is empty") 96 | _ = c.AbortWithError(http.StatusBadRequest, errors.New("UserID is empty")) 97 | } 98 | 99 | ProductID, err := primitive.ObjectIDFromHex(productQueryID) 100 | if err != nil { 101 | log.Println(err) 102 | c.AbortWithStatus(http.StatusInternalServerError) 103 | return 104 | } 105 | 106 | var ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) 107 | defer cancel() 108 | err = database.RemoveCartItem(ctx, app.prodCollection, app.userCollection, ProductID, userQueryID) 109 | if err != nil { 110 | c.IndentedJSON(http.StatusInternalServerError, err) 111 | return 112 | } 113 | c.IndentedJSON(200, "Successfully removed from cart") 114 | } 115 | } 116 | 117 | //function to get all items in the cart and total price 118 | //GET request 119 | //http://localhost:8000/listcart?id=xxxxxxuser_idxxxxxxxxxx 120 | //help to add dependency injection 121 | //Any nice way to group together this json response? 122 | func GetItemFromCart() gin.HandlerFunc { 123 | return func(c *gin.Context) { 124 | user_id := c.Query("id") 125 | if user_id == "" { 126 | c.Header("Content-Type", "application/json") 127 | c.JSON(http.StatusNotFound, gin.H{"error": "invalid id"}) 128 | c.Abort() 129 | return 130 | } 131 | 132 | usert_id, _ := primitive.ObjectIDFromHex(user_id) 133 | 134 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 135 | defer cancel() 136 | 137 | var filledcart models.User 138 | err := UserCollection.FindOne(ctx, bson.D{primitive.E{Key: "_id", Value: usert_id}}).Decode(&filledcart) 139 | if err != nil { 140 | log.Println(err) 141 | c.IndentedJSON(500, "not id found") 142 | return 143 | } 144 | 145 | filter_match := bson.D{{Key: "$match", Value: bson.D{primitive.E{Key: "_id", Value: usert_id}}}} 146 | unwind := bson.D{{Key: "$unwind", Value: bson.D{primitive.E{Key: "path", Value: "$usercart"}}}} 147 | grouping := bson.D{{Key: "$group", Value: bson.D{primitive.E{Key: "_id", Value: "$_id"}, {Key: "total", Value: bson.D{primitive.E{Key: "$sum", Value: "$usercart.price"}}}}}} 148 | pointcursor, err := UserCollection.Aggregate(ctx, mongo.Pipeline{filter_match, unwind, grouping}) 149 | if err != nil { 150 | log.Println(err) 151 | } 152 | var listing []bson.M 153 | if err = pointcursor.All(ctx, &listing); err != nil { 154 | log.Println(err) 155 | c.AbortWithStatus(http.StatusInternalServerError) 156 | } 157 | for _, json := range listing { 158 | c.IndentedJSON(200, json["total"]) 159 | c.IndentedJSON(200, filledcart.UserCart) 160 | } 161 | ctx.Done() 162 | } 163 | } 164 | 165 | func (app *Application) BuyFromCart() gin.HandlerFunc { 166 | return func(c *gin.Context) { 167 | userQueryID := c.Query("id") 168 | if userQueryID == "" { 169 | log.Panicln("user id is empty") 170 | _ = c.AbortWithError(http.StatusBadRequest, errors.New("UserID is empty")) 171 | } 172 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 173 | defer cancel() 174 | err := database.BuyItemFromCart(ctx, app.userCollection, userQueryID) 175 | if err != nil { 176 | c.IndentedJSON(http.StatusInternalServerError, err) 177 | } 178 | c.IndentedJSON(200, "Successfully Placed the order") 179 | } 180 | } 181 | 182 | func (app *Application) InstantBuy() gin.HandlerFunc { 183 | return func(c *gin.Context) { 184 | UserQueryID := c.Query("userid") 185 | if UserQueryID == "" { 186 | log.Println("UserID is empty") 187 | _ = c.AbortWithError(http.StatusBadRequest, errors.New("UserID is empty")) 188 | } 189 | ProductQueryID := c.Query("pid") 190 | if ProductQueryID == "" { 191 | log.Println("Product_ID id is empty") 192 | _ = c.AbortWithError(http.StatusBadRequest, errors.New("product_id is empty")) 193 | } 194 | productID, err := primitive.ObjectIDFromHex(ProductQueryID) 195 | if err != nil { 196 | log.Println(err) 197 | c.AbortWithStatus(http.StatusInternalServerError) 198 | return 199 | } 200 | 201 | var ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) 202 | // Always use the defer cancel() as close to the creation point as possible. 203 | defer cancel() 204 | err = database.InstantBuyer(ctx, app.prodCollection, app.userCollection, productID, UserQueryID) 205 | if err != nil { 206 | c.IndentedJSON(http.StatusInternalServerError, err) 207 | } 208 | c.IndentedJSON(200, "Successully placed the order") 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /controllers/controllers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "ecommerce/database" 6 | "ecommerce/models" 7 | generate "ecommerce/tokens" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/go-playground/validator/v10" 15 | "go.mongodb.org/mongo-driver/bson" 16 | "go.mongodb.org/mongo-driver/bson/primitive" 17 | "go.mongodb.org/mongo-driver/mongo" 18 | "golang.org/x/crypto/bcrypt" 19 | ) 20 | 21 | // Again, global vars, try to avoid them and use dependency injection instead. 22 | var UserCollection *mongo.Collection = database.UserData(database.Client, "Users") 23 | var ProductCollection *mongo.Collection = database.ProductData(database.Client, "Products") 24 | var Validate = validator.New() 25 | 26 | func HashPassword(password string) string { 27 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) 28 | if err != nil { 29 | log.Panic(err) 30 | } 31 | return string(bytes) 32 | } 33 | 34 | func VerifyPassword(userpassword string, givenpassword string) (bool, string) { 35 | err := bcrypt.CompareHashAndPassword([]byte(givenpassword), []byte(userpassword)) 36 | valid := true 37 | msg := "" 38 | if err != nil { 39 | msg = "Login Or Passowrd is Incorerct" 40 | valid = false 41 | } 42 | return valid, msg 43 | } 44 | 45 | // This actually indicates that you may need to seperate your controller.go into seperate files in your controllers package. 46 | /**********************************************************************************************/ 47 | 48 | //function to signup 49 | //accept a post request 50 | //POST Request 51 | //http://localhost:8000/users/signnup 52 | /* 53 | "fisrt_name":"joseph", 54 | "last_name":"hermis", 55 | "email":"something@gmail.com", 56 | "phone":"1156422222", 57 | "password":"hashed:)" 58 | 59 | */ 60 | func SignUp() gin.HandlerFunc { 61 | return func(c *gin.Context) { 62 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 63 | defer cancel() 64 | var user models.User 65 | if err := c.BindJSON(&user); err != nil { 66 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 67 | return 68 | } 69 | validationErr := Validate.Struct(user) 70 | if validationErr != nil { 71 | c.JSON(http.StatusBadRequest, gin.H{"error": validationErr}) 72 | return 73 | } 74 | 75 | count, err := UserCollection.CountDocuments(ctx, bson.M{"email": user.Email}) 76 | if err != nil { 77 | log.Panic(err) 78 | c.JSON(http.StatusInternalServerError, gin.H{"error": err}) 79 | return 80 | } 81 | if count > 0 { 82 | c.JSON(http.StatusBadRequest, gin.H{"error": "User already exists"}) 83 | } 84 | count, err = UserCollection.CountDocuments(ctx, bson.M{"phone": user.Phone}) 85 | defer cancel() 86 | if err != nil { 87 | log.Panic(err) 88 | c.JSON(http.StatusInternalServerError, gin.H{"error": err}) 89 | return 90 | } 91 | if count > 0 { 92 | c.JSON(http.StatusBadRequest, gin.H{"error": "Phone is already in use"}) 93 | return 94 | } 95 | // It doesn't make sense to hash the password while still checking if 96 | // the phone number exists or not. So I moved it down a bit. 97 | password := HashPassword(*user.Password) 98 | user.Password = &password 99 | 100 | user.Created_At, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) 101 | user.Updated_At, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) 102 | user.ID = primitive.NewObjectID() 103 | user.User_ID = user.ID.Hex() 104 | token, refreshtoken, _ := generate.TokenGenerator(*user.Email, *user.First_Name, *user.Last_Name, user.User_ID) 105 | user.Token = &token 106 | user.Refresh_Token = &refreshtoken 107 | user.UserCart = make([]models.ProductUser, 0) 108 | user.Address_Details = make([]models.Address, 0) 109 | user.Order_Status = make([]models.Order, 0) 110 | _, inserterr := UserCollection.InsertOne(ctx, user) 111 | if inserterr != nil { 112 | c.JSON(http.StatusInternalServerError, gin.H{"error": "not created"}) 113 | return 114 | } 115 | defer cancel() 116 | c.JSON(http.StatusCreated, "Successfully Signed Up!!") 117 | } 118 | } 119 | 120 | //function to generate login and check the user to create necessary fields in the db mostly as empty array 121 | // Accepts a POST 122 | /* 123 | "email":"lololol@sss.com" 124 | "password":"coollcollcoll" 125 | 126 | */ 127 | func Login() gin.HandlerFunc { 128 | return func(c *gin.Context) { 129 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 130 | defer cancel() 131 | var user models.User 132 | var founduser models.User 133 | if err := c.BindJSON(&user); err != nil { 134 | c.JSON(http.StatusBadRequest, gin.H{"error": err}) 135 | return 136 | } 137 | err := UserCollection.FindOne(ctx, bson.M{"email": user.Email}).Decode(&founduser) 138 | defer cancel() 139 | if err != nil { 140 | c.JSON(http.StatusInternalServerError, gin.H{"error": "login or password incorrect"}) 141 | return 142 | } 143 | PasswordIsValid, msg := VerifyPassword(*user.Password, *founduser.Password) 144 | defer cancel() 145 | if !PasswordIsValid { 146 | c.JSON(http.StatusInternalServerError, gin.H{"error": msg}) 147 | fmt.Println(msg) 148 | return 149 | } 150 | token, refreshToken, _ := generate.TokenGenerator(*founduser.Email, *founduser.First_Name, *founduser.Last_Name, founduser.User_ID) 151 | defer cancel() 152 | generate.UpdateAllTokens(token, refreshToken, founduser.User_ID) 153 | 154 | // Never ever ever ever send has password or hashed password to another application! 155 | 156 | // In your reddit post you say you want to make sure this github repo 157 | // is usefull to other beginners, but these kind of mistakes only 158 | // makes it for other beginners more difficult to handle passwords in 159 | // the correct way. 160 | c.JSON(http.StatusFound, founduser) 161 | 162 | } 163 | } 164 | 165 | //This is function to add products 166 | //this is an admin part 167 | //json should look like this 168 | // post request : http://localhost:8080/admin/addproduct 169 | /* 170 | json 171 | 172 | { 173 | "product_name" : "pencil" 174 | "price" : 98 175 | "rating" : 10 176 | "image" : "image-url" 177 | } 178 | */ 179 | func ProductViewerAdmin() gin.HandlerFunc { 180 | return func(c *gin.Context) { 181 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 182 | var products models.Product 183 | defer cancel() 184 | if err := c.BindJSON(&products); err != nil { 185 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 186 | return 187 | } 188 | products.Product_ID = primitive.NewObjectID() 189 | _, anyerr := ProductCollection.InsertOne(ctx, products) 190 | if anyerr != nil { 191 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Not Created"}) 192 | return 193 | } 194 | defer cancel() 195 | c.JSON(http.StatusOK, "Successfully added our Product Admin!!") 196 | } 197 | } 198 | 199 | // SearchProduct lists all the products in the database 200 | // paging will be added and fixed soon 201 | func SearchProduct() gin.HandlerFunc { 202 | return func(c *gin.Context) { 203 | var productlist []models.Product 204 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 205 | defer cancel() 206 | cursor, err := ProductCollection.Find(ctx, bson.D{{}}) 207 | if err != nil { 208 | c.IndentedJSON(http.StatusInternalServerError, "Someting Went Wrong Please Try After Some Time") 209 | return 210 | } 211 | err = cursor.All(ctx, &productlist) 212 | if err != nil { 213 | log.Println(err) 214 | c.AbortWithStatus(http.StatusInternalServerError) 215 | return 216 | } 217 | defer cursor.Close(ctx) 218 | if err := cursor.Err(); err != nil { 219 | // Don't forget to log errors. I log them really simple here just 220 | // to get the point across. 221 | log.Println(err) 222 | c.IndentedJSON(400, "invalid") 223 | return 224 | } 225 | defer cancel() 226 | c.IndentedJSON(200, productlist) 227 | 228 | } 229 | } 230 | 231 | // This is the function to search products based on alphabet name 232 | func SearchProductByQuery() gin.HandlerFunc { 233 | return func(c *gin.Context) { 234 | var searchproducts []models.Product 235 | queryParam := c.Query("name") 236 | if queryParam == "" { 237 | log.Println("query is empty") 238 | c.Header("Content-Type", "application/json") 239 | c.JSON(http.StatusNotFound, gin.H{"Error": "Invalid Search Index"}) 240 | c.Abort() 241 | return 242 | } 243 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 244 | defer cancel() 245 | searchquerydb, err := ProductCollection.Find(ctx, bson.M{"product_name": bson.M{"$regex": queryParam}}) 246 | if err != nil { 247 | c.IndentedJSON(404, "something went wrong in fetching the dbquery") 248 | return 249 | } 250 | err = searchquerydb.All(ctx, &searchproducts) 251 | if err != nil { 252 | log.Println(err) 253 | c.IndentedJSON(400, "invalid") 254 | return 255 | } 256 | defer searchquerydb.Close(ctx) 257 | if err := searchquerydb.Err(); err != nil { 258 | log.Println(err) 259 | c.IndentedJSON(400, "invalid request") 260 | return 261 | } 262 | defer cancel() 263 | c.IndentedJSON(200, searchproducts) 264 | } 265 | } 266 | 267 | /*****BLACKLIST************ 268 | func Logout() gin.HandlerFunc { 269 | return func(c *gin.Context) { 270 | user_id := c.Query("id") 271 | if user_id == "" { 272 | c.Header("Content-Type", "application-json") 273 | c.JSON(http.StatusNoContent, gin.H{"Error": "Invalid"}) 274 | c.Abort() 275 | return 276 | } 277 | usert_id, err := primitive.ObjectIDFromHex(user_id) 278 | if err != nil { 279 | c.IndentedJSON(500, "Something Went Wrong") 280 | } 281 | var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) 282 | defer cancel() 283 | filter := bson.D{{"_id", usert_id}} 284 | update := bson.D{{"$unset", bson.D{{"token", ""}, {"refresh_token", ""}}}} 285 | _, err = UserCollection.UpdateOne(ctx, filter, update) 286 | if err != nil { 287 | c.IndentedJSON(500, err) 288 | } 289 | 290 | } 291 | } 292 | //***************/ 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## **Fully functional ECOMMERCE API USING GIN FRAMEWORK AND MONGODB** 2 | 3 | drawing 4 | 5 | -----**Initial Release v2.30** ⚠️Not tested the efficiency 6 | 7 | # project structure 8 | 9 | - **Ecommerce** 📁 10 | - controllers 📁 11 | - controllers.go📝 12 | - database📁 13 | - database.go📝 14 | - middleware📁 15 | - middleware.go📝 16 | - models📁 17 | - models.go📝 18 | - routes📁 19 | - routes.go📝 20 | - tokens📁 21 | - tokengen.go📝 22 | - go.sum 📝 23 | - main.go📝 24 | 25 | clone this repo and change into the directory ecommerce-main*** 26 | make sure go is installed in your system 27 | Run 28 | 29 | ```bash 30 | # Start the mongodb container for local development 31 | docker-compose up -d 32 | go run main.go 33 | ``` 34 | 35 | #### ` Using Mongodb for small scale ecommerce industry is not a good idea instead use redis or mysql` 36 | 37 | ## API FUNCTIONALITY CURRENTLY ADDED? 38 | 39 | - Signup 🔒 40 | - Login 🔒 41 | - Product listing General View 👀 42 | - Adding the products to DB 43 | - Sorting the products from DB using regex 👀 44 | - Adding the products to cart 🛒 45 | - Removing the Product from cart🛒 46 | - Viewing the items in cart with total price🛒💰 47 | - Adding the Home Address 🏠 48 | - Adding the Work Address 🏢 49 | - Editing the Address ✂️ 50 | - Deleting the Adress 🗑️ 51 | - Checkout the Items from Cart 52 | - Buy Now products💰 53 | 54 | #### future implementations? 55 | 56 | - Pagination 1>>2>>3 57 | - Admin Part 58 | - OAuth 2 59 | - etc\*\*\* 60 | 61 | ## Code At glance in Database(database.go) 62 | 63 | first we need to define the database , make sure the mongodb installed in your system. 64 | The important thing we have to remember is we need to create a database with two collections , Two collections that for storing the user informations and the other for storing the product informations.We must have to configure the url and port of mongodb configured in your system 65 | 66 | ```go 67 | // code example of url and port in databasetup.go 68 | mongo.NewClient(options.Client().ApplyURI("mongodb://localhost:27017")) --port and url 69 | // code example of defining collection name 70 | var collection *mongo.Collection = client.Database("Ecommerce").Collection(CollectionName) 71 | ``` 72 | 73 | like to know more about configurations visit: https://www.mongodb.com/blog/post/quick-start-golang-mongodb-starting-and-setup 74 | 75 | ## Code At glance in models.go 76 | 77 | This part defines us how should our database looks like, I think its not about programming skills, but if you have creativity and basic syntax ideas and have some ability to defines struct from creativity 90% of work completed.Before Jumping into other codes we should have a rough idea about our plan so its better to lookup into models . 78 | 79 | - We have to define a product first , having a unique id , name and price 80 | 81 | ```go 82 | type Product struct { 83 | Product_ID primitive.ObjectID `bson:"_id"` 84 | Product_Name *string `json:"product_name"` 85 | Price *uint64 `json:"price"` 86 | Rating *uint8 `json:"rating"` 87 | Image *string `json:"image"` 88 | } 89 | ``` 90 | 91 | - We have to define an slice of array products where a user can store individual products 92 | 93 | ```go 94 | type ProductUser struct { 95 | Product_ID primitive.ObjectID `bson:"_id"` 96 | Product_Name *string `json:"product_name" bson:"product_name"` 97 | Price int `json:"price" bson:"price"` 98 | Rating *uint `json:"rating" bson:"rating"` 99 | Image *string `json:"image" bson:"image"` 100 | } 101 | ``` 102 | 103 | - The next struct we have to define the Address 104 | 105 | ```go 106 | type Address struct { 107 | Address_id primitive.ObjectID `bson:"_id"` 108 | House *string `json:"house_name" bson:"house_name"` 109 | Street *string `json:"street_name" bson:"street_name"` 110 | City *string `json:"city_name" bson:"city_name"` 111 | Pincode *string `json:"pin_code" bson:"pin_code"` 112 | } 113 | ``` 114 | 115 | - If the user has ordered something the struct look like the one in below, having an embedded struct inside a struct , here we define the ProductUser as a slice(A person can buy more than one product right?) and a payement struct to define Cash on delivery or digital payement 116 | 117 | ```go 118 | type Order struct { 119 | Order_ID primitive.ObjectID `bson:"_id"` 120 | Order_Cart []ProductUser `json:"order_list" bson:"order_list"` 121 | Orderered_At time.Time `json:"ordered_on" bson:"ordered_on"` 122 | Price int `json:"total_price" bson:"total_price"` 123 | Discount *int `json:"discount" bson:"discount"` 124 | Payment_Method Payment `json:"payment_method" bson:"payment_method"` 125 | } 126 | ``` 127 | 128 | The Payement struct is something look like this 129 | 130 | ```go 131 | type Payment struct { 132 | Digital bool `json:"digital" bson:"digital"` 133 | COD bool `json:"cod" bson:"cod"` 134 | } 135 | ``` 136 | 137 | **_we can define those structs as the simple fields or blocks (genrally known as documents and subdocuments in mongodb)in the user databse or the user struct_** 138 | 139 | - Finally in the user struct we are going to embedd the simple structs .The new fields in struct are ID, Name ,Email, Token etc 140 | 141 | ```go 142 | type User struct { 143 | ID primitive.ObjectID `json:"_id" bson:"_id"` 144 | First_Name *string `json:"first_name" validate:"required,min=2,max=30"` 145 | Last_Name *string `json:"last_name" validate:"required,min=2,max=30"` 146 | Password *string `json:"password" validate:"required,min=6"` 147 | Email *string `json:"email" validate:"email,required"` 148 | Phone *string `json:"phone" validate:"required"` 149 | Token *string `json:"token"` 150 | Refresh_Token *string `josn:"refresh_token"` 151 | Created_At time.Time `json:"created_at"` 152 | Updated_At time.Time `json:"updtaed_at"` 153 | User_ID string `json:"user_id"` 154 | UserCart []ProductUser `json:"usercart" bson:"usercart"` 155 | Address_Details []Address `json:"address" bson:"address"` 156 | Order_Status []Order `json:"orders" bson:"orders"` 157 | } 158 | ``` 159 | 160 | ## Code At Glance in controllers.go 161 | 162 | This file mainly describes about the token authentication process . We have used the JWT authentication from dgrijalwa but now the repository has changed . I have used the same implemtaion for signup and login from 163 | https://dev.to/joojodontoh/build-user-authentication-in-golang-with-jwt-and-mongodb-2igd , this blog is clear and precise about jwt auhentication rather than my explanation here. 164 | 165 | **_There is an important thing we have to remember when defining array struct the mongodb converts the array to a nil in document field_** 166 | 167 | So to overcome this problem we make an empty array in signup function like this,whever a user calls the signup function it initialise the documents to empty array 168 | 169 | ```go 170 | user.UserCart = make([]models.ProductUser, 0) 171 | user.Address_Details = make([]models.Address, 0) 172 | user.Order_Status = make([]models.Order, 0) 173 | ``` 174 | 175 | - **SIGNUP FUNCTION API CALL (POST REQUEST)** 176 | 177 | http://localhost:8000/users/signup 178 | 179 | ```json 180 | { 181 | "first_name": "Joseph", 182 | "last_name": "Hermis", 183 | "email": "joe@example.com", // Use @example.com for email because that is it's purpose. See https://www.rfc-editor.org/rfc/rfc6761.html and https://www.rfc-editor.org/rfc/rfc2606.html 184 | "password": "unlucky", 185 | "phone": "+1558426655" 186 | } 187 | ``` 188 | 189 | Response :"Successfully Signed Up!!" 190 | 191 | - **LOGIN FUNCTION API CALL (POST REQUEST)** 192 | 193 | http://localhost:8000/users/login 194 | 195 | 196 | ```json 197 | { 198 | "email": "joe@example.com", 199 | "password": "unlucky" 200 | } 201 | ``` 202 | 203 | response will be like this 204 | 205 | ```json 206 | { 207 | "_id": "***********************", 208 | "first_name": "joseph", 209 | "last_name": "hermis", 210 | "password": "$2a$14$UIYjkTfnFnhg4qhIfhtYnuK9qsBQifPKgu/WPZAYBaaN17j0eTQZa", 211 | "email": "josephhermis@protonomail.com", 212 | "phone": "+1558921455", 213 | "token": "eyJc0Bwcm90b25vbWFpbC5jb20iLCJGaXJzdF9OYW1lIjoiam9zZXBoIiwiTGFzdF9OYW1lIjoiaGVybWlzIiwiVWlkIjoiNjE2MTRmNTM5ZjI5YmU5NDJiZDlkZjhlIiwiZXhwIjoxNjMzODUzNjUxfQ.NbcpVtPLJJqRF44OLwoanynoejsjdJb5_v2qB41SmB8", 214 | "Refresh_Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFbWFpbCI6IiIsIkZpcnLCJVaWQiOiIiLCJleHAiOjE2MzQzNzIwNTF9.ocpU8-0gCJsejmCeeEiL8DXhFcZsW7Z3OCN34HgIf2c", 215 | "created_at": "2021-10-09T08:14:11Z", 216 | "updtaed_at": "2021-10-09T08:14:11Z", 217 | "user_id": "61614f539f29be942bd9df8e", 218 | "usercart": [], 219 | "address": [], 220 | "orders": [] 221 | } 222 | ``` 223 | 224 | Login Function call create an outlayer for our collection 225 | 226 | - **Admin add Product Function POST REQUEST** 227 | 228 | **note this function is not seperated from normal user fixed soon for admin** 229 | 230 | http://localhost:8000/admin/addproduct 231 | 232 | ```json 233 | { 234 | "product_name": "laptop", 235 | "price": 300, 236 | "rating": 10, 237 | "image": "1.jpg" 238 | } 239 | ``` 240 | 241 | Response : "Successfully added our Product Admin!!" 242 | 243 | - **View all the Products in db GET REQUEST** 244 | 245 | pagination added soon in next release 246 | 247 | http://localhost:8000/users/productview 248 | 249 | Response 250 | 251 | ```json 252 | [ 253 | { 254 | "Product_ID": "6153ff8edef2c3c0a02ae39a", 255 | "product_name": "notepad", 256 | "price": 50, 257 | "rating": 10, 258 | "image": "penc.jpg" 259 | }, 260 | { 261 | "Product_ID": "616152679f29be942bd9df8f", 262 | "product_name": "laptop", 263 | "price": 300, 264 | "rating": 10, 265 | "image": "1.jpg" 266 | }, 267 | { 268 | "Product_ID": "616152ee9f29be942bd9df90", 269 | "product_name": "top", 270 | "price": 300, 271 | "rating": 10, 272 | "image": "1.jpg" 273 | }, 274 | { 275 | "Product_ID": "616152fa9f29be942bd9df91", 276 | "product_name": "table", 277 | "price": 300, 278 | "rating": 10, 279 | "image": "1.jpg" 280 | }, 281 | { 282 | "Product_ID": "616153039f29be942bd9df92", 283 | "product_name": "apple", 284 | "price": 300, 285 | "rating": 10, 286 | "image": "1.jpg" 287 | } 288 | ] 289 | ``` 290 | 291 | - **Search Product by regex function (GET REQUEST)** 292 | 293 | defines the word search sorting 294 | http://localhost:8000/users/search?name=le 295 | 296 | response: 297 | 298 | ```json 299 | [ 300 | { 301 | "Product_ID": "616152fa9f29be942bd9df91", 302 | "product_name": "table", 303 | "price": 300, 304 | "rating": 10, 305 | "image": "1.jpg" 306 | }, 307 | { 308 | "Product_ID": "616153039f29be942bd9df92", 309 | "product_name": "apple", 310 | "price": 300, 311 | "rating": 10, 312 | "image": "1.jpg" 313 | } 314 | ] 315 | ``` 316 | 317 | The corresponding Query to mongodb is **ProductCollection.Find(ctx, bson.M{"product_name": bson.M{"$regex": queryParam}})** 318 | 319 | - **Adding the Products to the Cart (GET REQUEST)** 320 | 321 | http://localhost:8000/addtocart?id=xxxproduct_idxxx&userID=xxxxxxuser_idxxxxxx 322 | 323 | Corresponding mongodb query 324 | 325 | ```go 326 | filter := bson.D{primitive.E{Key: "_id", Value: id}} 327 | update := bson.D{{Key: "$push", Value: bson.D{primitive.E{Key: "usercart", Value: bson.D{{Key: "$each", Value: productcart}}}}}} 328 | _, err = UserCollection.UpdateOne(ctx, filter, update) 329 | ``` 330 | 331 | - **Removing Item From the Cart (GET REQUEST)** 332 | 333 | http://localhost:8000/addtocart?id=xxxxxxx&userID=xxxxxxxxxxxx 334 | 335 | Corresponding mongodb query 336 | 337 | ```go 338 | filter := bson.D{primitive.E{Key: "_id", Value: usert_id}} 339 | 340 | update := bson.M{"$pull": bson.M{"usercart": bson.M{"_id": removed_id}}} 341 | _, err = UserCollection.UpdateMany(ctx, filter, update) 342 | ``` 343 | 344 | - **Listing the item in the users cart (GET REQUEST) and total price** 345 | 346 | http://localhost:8000/listcart?id=xxxxxxuser_idxxxxxxxxxx 347 | 348 | Corresponding Mongodb Query (WE are using the aggrgate operation to find sum) 349 | 350 | filter_match := bson.D{{Key: "$match", Value: bson.D{primitive.E{Key: "_id", Value: usert_id}}}} 351 | unwind := bson.D{{Key: "$unwind", Value: bson.D{primitive.E{Key: "path", Value: "$usercart"}}}} 352 | grouping := bson.D{{Key: "$group", Value: bson.D{primitive.E{Key: "_id", Value: "$_id"}, {Key: "total", Value: bson.D{primitive.E{Key: "$sum", Value: "$usercart.price"}}}}}} 353 | pointcursor, err := UserCollection.Aggregate(ctx, mongo.Pipeline{filter_match, unwind, grouping}) 354 | 355 | - **Addding the Address (POST REQUEST)** 356 | 357 | http://localhost:8000/addadress?id=user_id**\*\***\***\*\*** 358 | 359 | The Address array is limited to two values home and work address more than two address is not acceptable 360 | 361 | ```json 362 | { 363 | "house_name": "jupyterlab", 364 | "street_name": "notebook", 365 | "city_name": "mars", 366 | "pin_code": "685607" 367 | } 368 | ``` 369 | 370 | - **Editing the Home Address(PUT REQUEST)** 371 | 372 | http://localhost:8000/edithomeaddress?id=xxxxxxxxxxuser_idxxxxxxxxxxxxxxx 373 | 374 | - **Editing the Work Address(PUT REQUEST)** 375 | 376 | http://localhost:8000/editworkaddress?id=xxxxxxxxxxuser_idxxxxxxxxxxxxxxx 377 | 378 | - **Delete Addresses(GET REQUEST)** 379 | 380 | http://localhost:8000/deleteaddresses?id=xxxxxxxxxuser_idxxxxxxxxxxxxx 381 | 382 | delete both addresses 383 | 384 | - **Cart Checkout Function and placing the order(GET REQUEST)** 385 | 386 | After placing the order the items have to be deleted from cart functonality added 387 | 388 | http://localhost:8000?id=xxuser_idxxx 389 | 390 | - **Instantly Buying the Products(GET REQUEST)** 391 | http://localhost:8000?userid=xxuser_idxxx&pid=xxxxproduct_idxxxx 392 | 393 | ## Code At Glance in main.go 394 | 395 | All the routes defined here requires the api authentication key 396 | 397 | Repo dedicting to my best friend mathappan 🐶 ,who left me without saying a goodbye and fill me with hopes even in my dark days RIP my friend 398 | 399 | #### Here are some of the reference I gone through while working on this project 400 | 401 | JWT Authentication using golang [^1] 402 | 403 | MongoDB Quick Introduction with Golang official blog [^2] 404 | 405 | Package context instead of goroutine [^3] 406 | 407 | Context use cases in real scenarios [^4] 408 | 409 | Mongo GO Driver official Documentation [^5] 410 | 411 | Mongo Go official go documentation [^6] 412 | 413 | Is "logout" useless on a REST API? [^7] 414 | 415 | JWT AUTHENTICATION OFFICIAL [^8] 416 | 417 | Gin framework Documentation [^9] 418 | 419 | [^1]: https://dev.to/joojodontoh/build-user-authentication-in-golang-with-jwt-and-mongodb-2igd 420 | [^2]: https://www.mongodb.com/blog/post/quick-start-golang-mongodb-starting-and-setup 421 | [^3]: https://pkg.go.dev/context 422 | [^4]: https://levelup.gitconnected.com/context-in-golang-98908f042a57 423 | [^5]: https://docs.mongodb.com/drivers/go/current/ 424 | [^6]: https://pkg.go.dev/go.mongodb.org/mongo-driver/mongo 425 | [^7]: https://stackoverflow.com/questions/36294359/is-logout-useless-on-a-rest-api 426 | [^8]: github.com/golang-jwt/jwt 427 | [^9]: https://github.com/gin-gonic/gin 428 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 8 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 9 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 10 | github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= 11 | github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 12 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 13 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 14 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 15 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 16 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 17 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 18 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 19 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 20 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 21 | github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= 22 | github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 23 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 24 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 25 | github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= 26 | github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= 27 | github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= 28 | github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 29 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 30 | github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= 31 | github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 32 | github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 33 | github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= 34 | github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= 35 | github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= 36 | github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= 37 | github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= 38 | github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= 39 | github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= 40 | github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= 41 | github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= 42 | github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 43 | github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 44 | github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 45 | github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 46 | github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= 47 | github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= 48 | github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= 49 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 50 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 51 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 52 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 53 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 54 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 56 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 57 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 58 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 59 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 60 | github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= 61 | github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= 62 | github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M= 63 | github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 64 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 65 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 66 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 67 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 68 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 69 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 70 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 71 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 72 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 73 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 74 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 75 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 76 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 77 | github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= 78 | github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= 79 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 80 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 81 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 82 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 83 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 84 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 85 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 86 | github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= 87 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 88 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 89 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 90 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 91 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 92 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 93 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 95 | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 96 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 97 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 98 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 99 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 100 | github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 101 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 102 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 103 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 104 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 105 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 106 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 107 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 108 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 109 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 110 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 111 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 112 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 113 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 114 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 115 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 116 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 117 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 118 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 119 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 120 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 121 | github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= 122 | github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= 123 | github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= 124 | github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= 125 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 126 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 127 | go.mongodb.org/mongo-driver v1.7.2 h1:pFttQyIiJUHEn50YfZgC9ECjITMT44oiN36uArf/OFg= 128 | go.mongodb.org/mongo-driver v1.7.2/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= 129 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 130 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 131 | golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 132 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 133 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 134 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 135 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 136 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 137 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 138 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 139 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 143 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 144 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 153 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 155 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 157 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 158 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 159 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 160 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 161 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 162 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 163 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 164 | golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 165 | golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 166 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 167 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 168 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 169 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 170 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 171 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 172 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 173 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 174 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 175 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 177 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 178 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 180 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 181 | --------------------------------------------------------------------------------