Repository: melardev/GoGonicEcommerceApi Branch: master Commit: 163dd9f2d15b Files: 45 Total size: 91.0 KB Directory structure: gitextract_mbl266ph/ ├── .gitignore ├── README.md ├── controllers/ │ ├── addresses.go │ ├── categories.go │ ├── comments.go │ ├── orders.go │ ├── pages.go │ ├── products.go │ ├── tags.go │ └── users.go ├── dtos/ │ ├── addresses.go │ ├── categories.go │ ├── comments.go │ ├── orders.go │ ├── pages.go │ ├── products.go │ ├── shared.go │ ├── tags.go │ └── users.go ├── infrastructure/ │ └── db.go ├── main.go ├── middlewares/ │ ├── auth.go │ ├── benchmark.go │ └── cors.go ├── models/ │ ├── address.go │ ├── category.go │ ├── comment.go │ ├── file_upload.go │ ├── order.go │ ├── order_item.go │ ├── product.go │ ├── product_category.go │ ├── product_tag.go │ ├── role.go │ ├── tag.go │ └── user.go ├── seeds/ │ └── seeder.go └── services/ ├── addresses.go ├── categories.go ├── comments.go ├── orders.go ├── products.go ├── shared.go ├── tags.go └── users.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /static/** .env app.db # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/modules.xml # .idea/*.iml # .idea/modules # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ================================================ FILE: README.md ================================================ # GoGonicEcommerceApi # Table of Contents - [Introduction](#introduction) - [Full-stack Applications](#full-stack-applications) * [E-commerce (shopping cart)](#e-commerce-shopping-cart) + [Server side implementations](#server-side-implementations) + [Client side implementations](#client-side-implementations) * [Blog/CMS](#blogcms) + [Server side implementations](#server-side-implementations-1) + [Client side](#client-side) - [The next come are](#the-next-come-are) * [Simple CRUD(Create, Read, Update, Delete)](#simple-crudcreate-read-update-delete) + [Server side implementations](#server-side-implementations-2) + [Client side implementations](#client-side-implementations-1) - [The next come are](#the-next-come-are-1) * [CRUD + Pagination](#crud--pagination) + [Server side implementations](#server-side-implementations-3) - [The next come are](#the-next-come-are-2) + [Client side implementations](#client-side-implementations-2) - [The next come are](#the-next-come-are-3) - [Follow me](#social-media-links) # Introduction This is one of my E-commerce API app implementations. It is written in Golang using go-gonic web framework.. This is not a finished project by any means, but it has a valid enough shape to be git cloned and studied if you are interested in this topic. If you are interested in this project take a look at my other server API implementations I have made with: # Full-stack Applications ## E-commerce (shopping cart) ### Server side implementations - [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SBootApiEcomMVCHibernate) - [Spring Boot + JAX-RS Jersey + Spring Data Hibernate](https://github.com/melardev/SpringBootEcommerceApiJersey) - [Node Js + Sequelize](https://github.com/melardev/ApiEcomSequelizeExpress) - [Node Js + Bookshelf](https://github.com/melardev/ApiEcomBookshelfExpress) - [Node Js + Mongoose](https://github.com/melardev/ApiEcomMongooseExpress) - [Python Django](https://github.com/melardev/DjangoRestShopApy) - [Flask](https://github.com/melardev/FlaskApiEcommerce) - [Golang go gonic](https://github.com/melardev/api_shop_gonic) - [Ruby on Rails](https://github.com/melardev/RailsApiEcommerce) - [AspNet Core](https://github.com/melardev/ApiAspCoreEcommerce) - [Laravel](https://github.com/melardev/ApiEcommerceLaravel) The next to come are: - Spring Boot + Spring Data Hibernate + Kotlin - Spring Boot + Jax-RS Jersey + Hibernate + Kotlin - Spring Boot + mybatis - Spring Boot + mybatis + Kotlin - Asp.Net Web Api v2 - Elixir - Golang + Beego - Golang + Iris - Golang + Echo - Golang + Mux - Golang + Revel - Golang + Kit - Flask + Flask-Restful - AspNetCore + NHibernate - AspNetCore + Dapper ### Client side implementations This client side E-commerce application is also implemented using other client side technologies: - [React Redux](https://github.com/melardev/ReactReduxEcommerceRestApi) - [React](https://github.com/melardev/ReactEcommerceRestApi) - [Vue](https://github.com/melardev/VueEcommerceRestApi) - [Vue + Vuex](https://github.com/melardev/VueVuexEcommerceRestApi) - [Angular](https://github.com/melardev/AngularEcommerceRestApi) ## Blog/CMS ### Server side implementations - [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SpringBootApiBlog) - [Go + Gin Gonic](https://github.com/melardev/GoGonicBlogApi) - [NodeJs + Mongoose](https://github.com/melardev/ApiBlogExpressMongoose) - [Laravel](https://github.com/melardev/LaravelApiBlog) - [Ruby on Rails + JBuilder](https://github.com/melardev/RailsApiBlog) - [Django + Rest-Framework](https://github.com/melardev/DjangoApiBlog) - [Asp.Net Core](https://github.com/melardev/AspCoreApiBlog) - [Flask + Flask-SQLAlchemy](https://github.com/melardev/FlaskApiBlog) The next to come are: - Spring Boot + Spring Data Hibernate + Kotlin - Spring Boot + Jax-RS Jersey + Hibernate + Kotlin - Spring Boot + mybatis - Spring Boot + mybatis + Kotlin - Asp.Net Web Api v2 - Elixir - Golang + Beego - Golang + Iris - Golang + Echo - Golang + Mux - Golang + Revel - Golang + Kit - Flask + Flask-Restful - AspNetCore + NHibernate - AspNetCore + Dapper ### Client side - [Vue + Vuex](https://github.com/melardev/VueVuexBlog) - [Vue](https://github.com/melardev/VueBlog) - [React + Redux](https://github.com/melardev/ReactReduxBlog) - [React](https://github.com/melardev/ReactBlog) - [Angular](https://github.com/melardev/AngularBlog) The next come are - Angular NgRx-Store - Angular + Material - React + Material - React + Redux + Material - Vue + Material - Vue + Vuex + Material - Ember ## Simple CRUD(Create, Read, Update, Delete) ### Server side implementations - [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SpringBootApiJpaCrud) - [Spring boot + Spring Data Reactive Mongo](https://github.com/melardev/SpringBootApiReactiveMongoCrud) - [Spring Boot + Spring Data Hibernate + Jersey](https://github.com/melardev/SpringBootApiJerseySpringDataCrud) - [NodeJs Express + Mongoose](https://github.com/melardev/ExpressMongooseApiCrud) - [Nodejs Express + Bookshelf](https://github.com/melardev/ExpressBookshelfApiCrud) - [Nodejs Express + Sequelize](https://github.com/melardev/ExpressSequelizeApiCrud) - [Go + Gin-Gonic + Gorm](https://github.com/melardev/GoGinGonicApiGormCrud) - [Ruby On Rails](https://github.com/melardev/RailsApiCrud) - [Ruby On Rails + JBuilder](https://github.com/melardev/RailsApiJBuilderCrud) - [Laravel](https://github.com/melardev/LaravelApiCrud) - [AspNet Core](https://github.com/melardev/AspNetCoreApiCrud) - [AspNet Web Api 2](https://github.com/melardev/AspNetWebApiCrud) - [Python + Flask](https://github.com/melardev/FlaskApiCrud) - [Python + Django](https://github.com/melardev/DjanogApiCrud) - [Python + Django + Rest Framework](https://github.com/melardev/DjangoRestFrameworkCrud) ### Client side implementations - [VueJs](https://github.com/melardev/VueAsyncCrud) #### The next come are - Angular NgRx-Store - Angular + Material - React + Material - React + Redux + Material - Vue + Material - Vue + Vuex + Material - Ember - Vanilla javascript ## CRUD + Pagination ### Server side implementations - [Spring Boot + Spring Data + Jersey](https://github.com/melardev/SpringBootJerseyApiPaginatedCrud) - [Spring Boot + Spring Data](https://github.com/melardev/SpringBootApiJpaPaginatedCrud) - [Spring Boot Reactive + Spring Data Reactive](https://github.com/melardev/ApiCrudReactiveMongo) - [Go with Gin Gonic](https://github.com/melardev/GoGinGonicApiPaginatedCrud) - [Laravel](https://github.com/melardev/LaravelApiPaginatedCrud) - [Rails + JBuilder](https://github.com/melardev/RailsJBuilderApiPaginatedCrud) - [Rails](https://github.com/melardev/RailsApiPaginatedCrud) - [NodeJs Express + Sequelize](https://github.com/melardev/ExpressSequelizeApiPaginatedCrud) - [NodeJs Express + Bookshelf](https://github.com/melardev/ExpressBookshelfApiPaginatedCrud) - [NodeJs Express + Mongoose](https://github.com/melardev/ExpressApiMongoosePaginatedCrud) - [Python Django](https://github.com/melardev/DjangoApiCrudPaginated) - [Python Django + Rest Framework](https://github.com/melardev/DjangoRestFrameworkPaginatedCrud) - [Python Flask](https://github.com/melardev/FlaskApiPaginatedCrud) - [AspNet Core](https://github.com/melardev/AspNetCoreApiPaginatedCrud) - [AspNet Web Api 2](https://github.com/melardev/WebApiPaginatedAsyncCrud) #### The next come are - NodeJs Express + Knex - Flask + Flask-Restful - Laravel + Fractal - Laravel + ApiResources - Go with Mux - AspNet Web Api 2 - Jersey - Elixir ### Client side implementations - [Angular](https://github.com/melardev/AngularPaginatedAsyncCrud) - [React-Redux](https://github.com/melardev/ReactReduxPaginatedAsyncCrud) - [React](https://github.com/melardev/ReactAsyncPaginatedCrud) - [Vue + Vuex](https://github.com/melardev/VueVuexPaginatedAsyncCrud) - [Vue](https://github.com/melardev/VuePaginatedAsyncCrud) #### The next come are - Angular NgRx-Store - Angular + Material - React + Material - React + Redux + Material - Vue + Material - Vue + Vuex + Material - Ember - Vanilla javascript # Social media links - [Youtube Channel](https://youtube.com/melardev) I publish videos mainly on programming - [Blog](http://melardev.com) Sometimes I publish the source code there before Github - [Twitter](https://twitter.com/@melardev) I share tips on programming ## WARNING I have mass of projects to deal with so I make some copy/paste around, if something I say is missing or is wrong, then I apologize and you may let me know opening an issue. # Getting started 1. go get https://github.com/melardev/ApiEcomGoGonic 1. Change the .env.example as you need(see warning below) 1. Rename .env.example to .env 1. Seed the database passing "create seed" as arguments to the app(read main.go to understand what I mean) ## WARNING The recommended database to use is Postgresql, the other database backends may not work as expected. Unfortunately the MySQL does not work as expected, for example the BeforeSave Hook for User is not able to retrieve the Role model if using MySQL, the same code does work if SQLite, it is weird, because the SQL query generated is valid and it returns a row, but somehow the driver is not able to map it to the user. # Features - Authentication / Authorization - JWT middleware for authentication - Multi file upload - Database seed - Paging with Limit and Offset using GORM (Golang ORM framework) - CRUD operations on products, comments, tags, categories, orders ![Fetching products page](./github_images/postman.png) - Orders, guest users may place an order ![Database diagram](./github_images/db_structure.png) # What you will learn - Golang - Golang Go-Gonic web framework - JWT - Controllers - Middlewares - JWT Authentication - Role based authorization - GORM - associations: ManyToMany, OneToMany, ManyToOne - virtual fields - Select specific columns - Eager loading - Count related association - seed data - misc - project structure # Understanding the project The project is meant to be educational, to learn something beyond the hello world thing we find in a lot, lot of tutorials and blog posts. Since its main goal is educational, I try to make as much use as features of APIs, in other words, I used different code to do the same thing over and over, there is some repeated code but I tried to be as unique as possible so you can learn different ways of achieving the same goal. Project structure: - models: Mvc, it is our domain data. - dtos: it contains our serializers, they will create the response to be sent as json. They also take care of validating the input(feature incomplete) - controllers: well this is the mvC, they receive the request from the user, they ask the services to perform an action for them on the database. - seeds: contains the file that seeds the database. - static: a folder that will be generated when you create a product or tag or category with images - services: contains some business logic for each model, and for authorization - middlewares: it contains middlewares(golang functions) that are triggered before the controller action, for example, a middleware which reads the request looking for the Jwt token and trying to authenticate the user before forwarding the request to the corresponding controller action # TODO - Add model constraints such as not null - Refactor the seeding with http://gorm.io/docs/query.html#Select - Global Application Error handling - Can't Preload field errors: - Get comment details http://127.0.0.1:8080/api/products/:slug/comments/:id triggered in services.FetchCommentById - Get My Orders http://localhost:8080/api/orders triggered with services.FetchOrdersPage - Security, validations, file upload - Delete FileUpload if associated tag, category or product deleted - Delete Files if tag, category, product fail to be saved - Use pointers as function parameters instead of passing them by value as I did in many - For some reason /api/products does not work on browsers due to CORS issues, /api/home does work, on postman all routes work .... # Resources - [Go-Gonic](https://github.com/gin-gonic/gin) Awesome golang based web framework - [GORM]() - [CORS gin's middleware](https://github.com/gin-contrib/cors) ================================================ FILE: controllers/addresses.go ================================================ package controllers import ( "github.com/gin-gonic/gin" "github.com/melardev/GoGonicEcommerceApi/dtos" "github.com/melardev/GoGonicEcommerceApi/middlewares" "github.com/melardev/GoGonicEcommerceApi/models" "github.com/melardev/GoGonicEcommerceApi/services" "net/http" "strconv" ) func RegisterAddressesRoutes(router *gin.RouterGroup) { router.Use(middlewares.EnforceAuthenticatedMiddleware()) { router.GET("/addresses", ListAddresses) router.POST("/addresses", CreateAddress) } } func ListAddresses(c *gin.Context) { pageSizeStr := c.Query("page_size") pageStr := c.Query("page") pageSize, err := strconv.Atoi(pageSizeStr) if err != nil { pageSize = 5 } page, err := strconv.Atoi(pageStr) if err != nil { page = 1 } // userId:= c.Keys["currentUserId"].(uint) // or userId := c.MustGet("currentUserId").(uint) includeUser := false addresses, totalCommentCount := services.FetchAddressesPage(userId, page, pageSize, includeUser) c.JSON(http.StatusOK, dtos.CreateAddressPagedResponse(c.Request, addresses, page, pageSize, totalCommentCount, includeUser)) } func CreateAddress(c *gin.Context) { user := c.MustGet("currentUser").(models.User) var json dtos.CreateAddress if err := c.ShouldBindJSON(&json); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err)) return } firstName := json.FirstName lastName := json.LastName if firstName == "" { firstName = user.FirstName } if lastName == "" { lastName = user.LastName } address := models.Address{ FirstName: firstName, LastName: lastName, Country: json.Country, City: json.City, StreetAddress: json.StreetAddress, ZipCode: json.ZipCode, User: user, UserId: user.ID, } if err := services.SaveOne(&address); err != nil { c.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto("database_error", err)) return } c.JSON(http.StatusOK, dtos.GetAddressCreatedDto(&address, false)) } ================================================ FILE: controllers/categories.go ================================================ package controllers import ( "fmt" "github.com/gin-gonic/gin" "github.com/melardev/GoGonicEcommerceApi/dtos" "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/middlewares" "github.com/melardev/GoGonicEcommerceApi/models" "github.com/melardev/GoGonicEcommerceApi/services" "io" "log" "net/http" "os" "path/filepath" ) func RegisterCategoryRoutes(router *gin.RouterGroup) { router.GET("", CategoryList) router.Use(middlewares.EnforceAuthenticatedMiddleware()) { router.POST("", CreateCategory) } } func CategoryList(c *gin.Context) { tags, err := services.FetchAllCategories() if err != nil { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("fetch_error", err)) return } c.JSON(http.StatusOK, dtos.CreateCategoryListMapDto(tags)) } func CreateCategory(c *gin.Context) { user := c.MustGet("currentUser").(models.User) if user.IsNotAdmin() { c.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage("Permission denied, you must be admin")) return } name := c.PostForm("name") description := c.PostForm("description") form, err := c.MultipartForm() if err != nil { c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error())) return } files := form.File["images[]"] var categoryImages = make([]models.FileUpload, len(files)) for index, file := range files { fileName := randomString(16) + ".png" dirPath := filepath.Join(".", "static", "images", "categories") filePath := filepath.Join(dirPath, fileName) // Create directory if does not exist if _, err = os.Stat(dirPath); os.IsNotExist(err) { err = os.MkdirAll(dirPath, os.ModeDir) if err != nil { c.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto("io_error", err)) return } } // Create file that will hold the image outputFile, err := os.Create(filePath) if err != nil { log.Fatal(err) } defer outputFile.Close() // Open the temporary file that contains the uploaded image inputFile, err := file.Open() if err != nil { c.JSON(http.StatusOK, dtos.CreateDetailedErrorDto("io_error", err)) } defer inputFile.Close() // Copy the temporary image to the permanent location outputFile _, err = io.Copy(outputFile, inputFile) if err != nil { log.Fatal(err) c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) return } fileSize := (uint)(file.Size) categoryImages[index] = models.FileUpload{Filename: file.Filename, FilePath: string(filepath.Separator) + filePath, FileSize: fileSize} } database := infrastructure.GetDb() category := models.Category{Name: name, Description: description, Images: categoryImages} // TODO: Why it is performing a SELECT SQL Query per image? // Even worse, it is selecting category_id, why?? // SELECT "tag_id", "product_id" FROM "file_uploads" WHERE (id = insertedFileUploadId) err = database.Create(&category).Error if err != nil { c.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto("db_error", err)) } c.JSON(http.StatusOK, dtos.CreateCategoryCreatedDto(category)) } ================================================ FILE: controllers/comments.go ================================================ package controllers import ( "errors" "github.com/gin-gonic/gin" "github.com/melardev/GoGonicEcommerceApi/dtos" "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/middlewares" "github.com/melardev/GoGonicEcommerceApi/models" "github.com/melardev/GoGonicEcommerceApi/services" "net/http" "strconv" ) func RegisterCommentRoutes(router *gin.RouterGroup) { router.GET("/products/:slug/comments", ListComments) router.GET("/products/:slug/comments/:id", ShowComment) router.GET("/comments/:id", ShowComment) router.Use(middlewares.EnforceAuthenticatedMiddleware()) { router.POST("/products/:slug/comments", CreateComment) router.DELETE("/comments/:id", DeleteComment) router.DELETE("/products/:slug/comments/:id", DeleteComment) } } func ListComments(c *gin.Context) { slug := c.Param("slug") database := infrastructure.GetDb() productId := -1 err := database.Model(&models.Product{}).Where(&models.Product{Slug: slug}).Select("id").Row().Scan(&productId) if err != nil { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("comments", errors.New("invalid slug"))) return } pageSizeStr := c.Query("page_size") pageStr := c.Query("page") pageSize, err := strconv.Atoi(pageSizeStr) if err != nil { pageSize = 5 } page, err := strconv.Atoi(pageStr) if err != nil { page = 1 } comments, totalCommentCount := services.FetchCommentsPage(productId, page, pageSize) c.JSON(http.StatusOK, dtos.CreateCommentPagedResponse(c.Request, comments, page, pageSize, totalCommentCount, true, false)) } func CreateComment(c *gin.Context) { slug := c.Param("slug") if slug == "" { c.JSON(http.StatusBadRequest, dtos.CreateErrorDtoWithMessage("You must provide a product slug you want to comment")) return } var json dtos.CreateComment if err := c.ShouldBindJSON(&json); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err)) return } productId, err := services.FetchProductId(slug) if err != nil { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("database_error", err)) return } comment := models.Comment{ Content: json.Content, ProductId: productId, User: c.MustGet("currentUser").(models.User), UserId: c.MustGet("currentUserId").(uint), } if err := services.SaveOne(&comment); err != nil { c.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto("database_error", err)) return } c.JSON(http.StatusOK, dtos.CreateCommentCreatedDto(&comment)) } func ShowComment(c *gin.Context) { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil { c.JSON(http.StatusBadRequest, dtos.CreateErrorDtoWithMessage("You must provide a valid comment id")) } comment := services.FetchCommentById(id, true, true) c.JSON(http.StatusOK, dtos.GetCommentDetailsDto(&comment, true, true)) } func DeleteComment(c *gin.Context) { currentUser := c.MustGet("currentUser").(models.User) id64, err := strconv.ParseUint(c.Param("id"), 10, 32) id := uint(id64) database := infrastructure.GetDb() var comment models.Comment err = database.Select([]string{"id", "user_id"}).Find(&comment, id).Error if err != nil || comment.ID == 0 { // the comment.ID == is redundat, but shows the other way of checking but it is less readable c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("comment", err)) } else if currentUser.ID == comment.UserId || currentUser.IsAdmin() { err = database.Delete(&comment).Error if err != nil { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("database_error", err)) return } c.JSON(http.StatusOK, dtos.CreateSuccessWithMessageDto("Comment Deleted successfully")) } else { c.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage("You have to be admin or the owner of this comment to delete it")) } } ================================================ FILE: controllers/orders.go ================================================ package controllers import ( "github.com/gin-gonic/gin" "github.com/melardev/GoGonicEcommerceApi/dtos" "github.com/melardev/GoGonicEcommerceApi/middlewares" "github.com/melardev/GoGonicEcommerceApi/models" "github.com/melardev/GoGonicEcommerceApi/services" "net/http" "strconv" ) func RegisterOrderRoutes(router *gin.RouterGroup) { router.POST("", CreateOrder) router.Use(middlewares.EnforceAuthenticatedMiddleware()) { router.GET("", ListOrders) router.GET("/:id", ShowOrder) } } func ListOrders(c *gin.Context) { pageSizeStr := c.Query("page_size") pageStr := c.Query("page") pageSize, err := strconv.Atoi(pageSizeStr) if err != nil { pageSize = 5 } page, err := strconv.Atoi(pageStr) if err != nil { page = 1 } userId := c.MustGet("currentUserId").(uint) orders, totalCommentCount, err := services.FetchOrdersPage(userId, page, pageSize) c.JSON(http.StatusOK, dtos.CreateOrderPagedResponse(c.Request, orders, page, pageSize, totalCommentCount, false, false)) } func ShowOrder(c *gin.Context) { orderId, err := strconv.Atoi(c.Param("id")) user := c.MustGet("currentUser").(models.User) order, err := services.FetchOrderDetails(uint(orderId)) if err != nil { c.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto("db_error", err)) return } if order.UserId == user.ID || user.IsAdmin() { c.JSON(http.StatusOK, dtos.CreateOrderDetailsDto(&order)) } else { c.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage("Permission denied, you can not view this order")) return } } func CreateOrder(c *gin.Context) { var orderRequest dtos.CreateOrderRequestDto if err := c.ShouldBind(&orderRequest); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err)) return } userObj, userLoggedIn := c.Get("currentUser") var user models.User if userLoggedIn { user = (userObj).(models.User) } var address models.Address // Reuse address can only be done by authenticated users if orderRequest.AddressId != 0 && userLoggedIn { address = services.FetchAddress(orderRequest.AddressId) /*if err != nil || address.ID == 0 { c.JSON(http.StatusBadRequest, dtos.CreateDetailedErrorDto("db_error", err)) return }*/ if address.UserId != user.ID { c.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage("permission denied")) return } } else if orderRequest.AddressId == 0 { address = models.Address{ FirstName: orderRequest.FirstName, LastName: orderRequest.LastName, City: orderRequest.City, Country: orderRequest.Country, StreetAddress: orderRequest.StreetAddress, ZipCode: orderRequest.ZipCode, } if userLoggedIn { address.UserId = user.ID } err := services.CreateOne(&address) if err != nil { c.JSON(http.StatusInternalServerError, err) return } } else { c.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage("Operation not supported, what are you trying to do?")) return } order := models.Order{ TrackingNumber: randomString(16), OrderStatus: 0, Address: address, AddressId: address.ID, } if userLoggedIn { order.UserId = user.ID order.User = user } var productIds = make([]uint, len(orderRequest.CartItems)) for i := 0; i < len(orderRequest.CartItems); i++ { productIds[i] = orderRequest.CartItems[i].Id } products, err := services.FetchProductsIdNameAndPrice(productIds) if err != nil { c.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto("db_error", err)) return } if len(products) != len(orderRequest.CartItems) { c.JSON(http.StatusUnprocessableEntity, dtos.CreateErrorDtoWithMessage("make sure all products are still available")) return } orderItems := make([]models.OrderItem, len(products)) for i := 0; i < len(products); i++ { // I am assuming product ids returned are in the same order as the cart_items, TODO: implement a more robust code to ensure orderItems[i] = models.OrderItem{ ProductId: products[i].ID, ProductName: products[i].Name, Slug: products[i].Slug, Quantity: orderRequest.CartItems[i].Quantity, } } order.OrderItems = orderItems err = services.CreateOne(&order) if err != nil { c.JSON(http.StatusInternalServerError, err) return } c.JSON(http.StatusOK, dtos.CreateOrderCreatedDto(&order)) } ================================================ FILE: controllers/pages.go ================================================ package controllers import ( "errors" "github.com/gin-gonic/gin" "github.com/melardev/GoGonicEcommerceApi/dtos" "github.com/melardev/GoGonicEcommerceApi/services" "net/http" ) func RegisterPageRoutes(router *gin.RouterGroup) { router.GET("", Home) router.GET("/home", Home) } func Home(c *gin.Context) { tags, err := services.FetchAllTags() categories, err := services.FetchAllCategories() if err != nil { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("comments", errors.New("Somethign went wrong"))) return } c.JSON(http.StatusOK, dtos.CreateHomeResponse(tags, categories)) } ================================================ FILE: controllers/products.go ================================================ package controllers // import "C" import ( "errors" "github.com/gin-gonic/gin" "github.com/gosimple/slug" "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" "os" "path/filepath" "regexp" "strings" "github.com/melardev/GoGonicEcommerceApi/dtos" "github.com/melardev/GoGonicEcommerceApi/middlewares" "github.com/melardev/GoGonicEcommerceApi/services" "net/http" "strconv" ) func RegisterProductRoutes(router *gin.RouterGroup) { router.GET("/", ProductList) router.GET("/:slug", GetProductDetailsBySlug) router.Use(middlewares.EnforceAuthenticatedMiddleware()) { router.POST("/", CreateProduct) router.DELETE("/:slug", ProductDelete) } } func ProductList(c *gin.Context) { pageSizeStr := c.Query("page_size") pageStr := c.Query("page") pageSize, err := strconv.Atoi(pageSizeStr) if err != nil { pageSize = 5 } page, err := strconv.Atoi(pageStr) if err != nil { page = 1 } productModels, modelCount, commentsCount, err := services.FetchProductsPage(page, pageSize) if err != nil { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("products", errors.New("Invalid param"))) return } c.JSON(http.StatusOK, dtos.CreatedProductPagedResponse(c.Request, productModels, page, pageSize, modelCount, commentsCount)) } func GetProductDetailsBySlug(c *gin.Context) { productSlug := c.Param("slug") product := services.FetchProductDetails(&models.Product{Slug: productSlug}, true) if product.ID == 0 { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("products", errors.New("Invalid slug"))) return } c.JSON(http.StatusOK, dtos.CreateProductDetailsDto(product)) } func CreateProduct(c *gin.Context) { // Only admin users can create products user := c.Keys["currentUser"].(models.User) if user.IsNotAdmin() { c.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage("Permission denied, you must be admin")) return } var formDto dtos.CreateProduct if err := c.ShouldBind(&formDto); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err)) return } name := formDto.Name description := formDto.Description price := formDto.Price stock, err := strconv.ParseInt(c.PostForm("stock"), 10, 32) form, err := c.MultipartForm() tagCount := 0 catCount := 0 for key := range form.Value { if strings.HasPrefix(key, "tags[") { tagCount++ } if strings.HasPrefix(key, "category[") { catCount++ } } var tags = make([]models.Tag, tagCount) var categories = make([]models.Category, catCount) var rgx = regexp.MustCompile(`\[(.*?)\]`) database := infrastructure.GetDb() tagPtr := 0 catPtr := 0 for k, v := range form.Value { if strings.HasPrefix(k, "tags[") { result := rgx.FindStringSubmatch(k) var tag models.Tag name := result[1] description := v[0] database.Where(&models.Tag{Slug: slug.Make(name)}). Attrs(models.Tag{Name: name, Description: description}). FirstOrCreate(&tag) tags[tagPtr] = tag tagPtr++ } if strings.HasPrefix(k, "category[") { result := rgx.FindStringSubmatch(k) var category models.Category name := result[1] description := v[0] database.Where(&models.Category{Slug: slug.Make(name)}). Attrs(models.Category{Name: name, Description: description}). FirstOrCreate(&category) categories[catPtr] = category catPtr++ } } if err != nil { c.JSON(http.StatusBadRequest, dtos.CreateDetailedErrorDto("form_error", err)) return } files := form.File["images[]"] var productImages = make([]models.FileUpload, len(files)) for index, file := range files { fileName := randomString(16) + ".png" dirPath := filepath.Join(".", "static", "images", "products") filePath := filepath.Join(dirPath, fileName) if _, err = os.Stat(dirPath); os.IsNotExist(err) { err = os.MkdirAll(dirPath, os.ModeDir) if err != nil { c.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto("io_error", err)) return } } if err := c.SaveUploadedFile(file, filePath); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateDetailedErrorDto("upload_error", err)) return } fileSize := (uint)(file.Size) productImages[index] = models.FileUpload{Filename: fileName, OriginalName: file.Filename, FilePath: string(filepath.Separator) + filePath, FileSize: fileSize} } product := models.Product{ Name: name, Description: description, Tags: tags, Categories: categories, Price: (int)(price), Stock: (int)(stock), Images: productImages, } if err := services.CreateOne(&product); err != nil { c.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto("database", err)) return } c.JSON(http.StatusOK, dtos.CreateProductCreatedDto(product)) } func ProductDelete(c *gin.Context) { slug := c.Param("slug") err := services.DeleteProduct(&models.Product{Slug: slug}) if err != nil { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("products", errors.New("Invalid slug"))) return } c.JSON(http.StatusOK, gin.H{"product": "Delete success"}) } ================================================ FILE: controllers/tags.go ================================================ package controllers import ( "fmt" "github.com/gin-gonic/gin" "github.com/melardev/GoGonicEcommerceApi/dtos" "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/middlewares" "github.com/melardev/GoGonicEcommerceApi/models" "github.com/melardev/GoGonicEcommerceApi/services" "math/rand" "net/http" "os" "path/filepath" ) func RegisterTagRoutes(router *gin.RouterGroup) { router.GET("", TagList) router.Use(middlewares.EnforceAuthenticatedMiddleware()) { router.POST("", CreateTag) } } func TagList(c *gin.Context) { tags, err := services.FetchAllTags() if err != nil { c.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto("fetch_error", err)) return } c.JSON(http.StatusOK, dtos.CreateTagListMapDto(tags)) } var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randomString(length int) string { b := make([]rune, length) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) } func CreateTag(c *gin.Context) { user := c.Keys["currentUser"].(models.User) if user.IsNotAdmin() { c.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage("Permission denied, you must be admin")) return } var createForm dtos.CreateTag // name := c.PostForm("name") // description := c.PostForm("description") // If you wanna know more about how binding is done internally check gin-gonic/bin/binding.formBinding.Bind at form.go if err := c.ShouldBind(&createForm); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err)) return } form, err := c.MultipartForm() if err != nil { c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error())) return } files := form.File["images[]"] var tagImages = make([]models.FileUpload, len(files)) for index, file := range files { fileName := randomString(16) + ".png" dirPath := filepath.Join(".", "static", "images", "tags") filePath := filepath.Join(dirPath, fileName) if _, err = os.Stat(dirPath); os.IsNotExist(err) { err = os.MkdirAll(dirPath, os.ModeDir) if err != nil { c.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto("io_error", err)) return } } if err := c.SaveUploadedFile(file, filePath); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateDetailedErrorDto("upload_error", err)) return } fileSize := (uint)(file.Size) tagImages[index] = models.FileUpload{Filename: fileName, OriginalName: file.Filename, FilePath: string(filepath.Separator) + filePath, FileSize: fileSize} } database := infrastructure.GetDb() tag := models.Tag{Name: createForm.Name, Description: createForm.Description, Images: tagImages} // TODO: Why it is performing a SELECT SQL Query per image? // Even worse, it is selecting category_id, why?? // SELECT "category_id", "product_id" FROM "file_uploads" WHERE (id = insertedFileUploadId) err = database.Create(&tag).Error if err != nil { c.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto("db_error", err)) } c.JSON(http.StatusOK, dtos.CreateTagCreatedDto(tag)) } ================================================ FILE: controllers/users.go ================================================ package controllers import ( "errors" "github.com/gin-gonic/gin" "github.com/melardev/GoGonicEcommerceApi/dtos" "github.com/melardev/GoGonicEcommerceApi/services" "github.com/melardev/GoGonicEcommerceApi/models" "golang.org/x/crypto/bcrypt" "net/http" ) func RegisterUserRoutes(router *gin.RouterGroup) { router.POST("/", UsersRegistration) router.POST("/login", UsersLogin) } func UsersRegistration(c *gin.Context) { var json dtos.RegisterRequestDto if err := c.ShouldBindJSON(&json); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err)) return } password, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost) if err := services.CreateOne(&models.User{ Username: json.Username, Password: string(password), FirstName: json.FirstName, LastName: json.LastName, Email: json.Email, }); err != nil { c.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto("database", err)) return } c.JSON(http.StatusCreated, gin.H{ "success": true, "full_messages": []string{"User created successfully"}}) } func UsersLogin(c *gin.Context) { var json dtos.LoginRequestDto if err := c.ShouldBindJSON(&json); err != nil { c.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err)) return } user, err := services.FindOneUser(&models.User{Username: json.Username}) if err != nil { c.JSON(http.StatusForbidden, dtos.CreateDetailedErrorDto("login_error", err)) return } if user.IsValidPassword(json.Password) != nil { c.JSON(http.StatusForbidden, dtos.CreateDetailedErrorDto("login", errors.New("invalid credentials"))) return } c.JSON(http.StatusOK, dtos.CreateLoginSuccessful(&user)) } ================================================ FILE: dtos/addresses.go ================================================ package dtos import ( "github.com/melardev/GoGonicEcommerceApi/models" "net/http" ) type CreateAddress struct { FirstName string `form:"first_name" json:"first_name" xml:"first_name"` LastName string `form:"last_name" json:"last_name" xml:"last_name"` Country string `form:"country" json:"country" xml:"country" binding:"required"` City string `form:"city" json:"city" xml:"city" binding:"required"` StreetAddress string `form:"address" json:"address" xml:"address" binding:"required"` ZipCode string `form:"zip_code" json:"zip_code" xml:"zip_code" binding:"required"` } func CreateAddressPagedResponse(request *http.Request, addresses []models.Address, page, page_size, count int, includeUser bool) map[string]interface{} { var resources = make([]interface{}, len(addresses)) for index, address := range addresses { resources[index] = GetAddressDto(&address, includeUser) } return CreatePagedResponse(request, resources, "addresses", page, page_size, count) } func GetAddressDto(address *models.Address, includeUser bool) map[string]interface{} { dto := map[string]interface{}{ "id": address.ID, "first_name": address.FirstName, "last_name": address.LastName, "zip_code": address.ZipCode, "country": address.Country, "city": address.City, } if includeUser { dto["user"] = map[string]interface{}{ "id": address.UserId, "username": address.User.Username, } } return dto } func GetAddressCreatedDto(address *models.Address, includeUser bool) map[string]interface{} { return CreateSuccessWithDtoAndMessageDto(GetAddressDto(address, includeUser), "StreetAddress created successfully") } ================================================ FILE: dtos/categories.go ================================================ package dtos import ( "github.com/melardev/GoGonicEcommerceApi/models" "strings" ) func CreateCategoryListMapDto(categories []models.Category) map[string]interface{} { result := map[string]interface{}{} var t = make([]interface{}, len(categories)) for i := 0; i < len(categories); i++ { t[i] = CreateCategoryDto(categories[i]) } result["categories"] = t return CreateSuccessDto(result) } func CreateCategoryListDto(categories []models.Category) []interface{} { var t = make([]interface{}, len(categories)) for i := 0; i < len(categories); i++ { t[i] = CreateCategoryDto(categories[i]) } return t } func CreateCategoryDto(category models.Category) map[string]interface{} { var imageUrls = make([]string, len(category.Images)) replaceAllFlag := -1 for i := 0; i < len(category.Images); i++ { imageUrls[i] = strings.Replace(category.Images[i].FilePath, "\\", "/", replaceAllFlag) } return map[string]interface{}{ "id": category.ID, "name": category.Name, "description": category.Description, "image_urls": imageUrls, } } func CreateCategoryCreatedDto(category models.Category) map[string]interface{} { return CreateSuccessWithDtoAndMessageDto(CreateCategoryDto(category), "Category created successfully") } ================================================ FILE: dtos/comments.go ================================================ package dtos import ( "github.com/melardev/GoGonicEcommerceApi/models" "net/http" "time" ) type CreateComment struct { Content string `form:"content" json:"content" xml:"content" binding:"required"` } func CreateCommentPagedResponse(request *http.Request, comments []models.Comment, page, page_size, count int, bools ...bool) map[string]interface{} { var resources = make([]interface{}, len(comments)) for index, comment := range comments { includeUser := false if len(bools) > 0 { includeUser = bools[0] } includeProduct := false if len(bools) > 1 { includeProduct = bools[1] } resources[index] = GetSummary(&comment, includeUser, includeProduct) } return CreatePagedResponse(request, resources, "comments", page, page_size, count) } func GetCommentDetailsDto(comment *models.Comment, includes ...bool) map[string]interface{} { includeUser := false if len(includes) > 0 { includeUser = includes[0] } includeProduct := false if len(includes) > 1 { includeProduct = includes[1] } return GetSummary(comment, includeUser, includeProduct) } func GetSummary(comment *models.Comment, includeUser, includeProduct bool) map[string]interface{} { result := map[string]interface{}{ "id": comment.ID, "content": comment.Content, "created_at": comment.CreatedAt.UTC().Format(time.RFC1123), "updated_at": comment.UpdatedAt.UTC().Format(time.RFC1123), } if includeUser == true { result["user"] = map[string]interface{}{ "id": comment.User.ID, "username": comment.User.Username, } } if includeProduct == true { result["product"] = map[string]interface{}{ "id": comment.Product.ID, "name": comment.Product.Name, "slug": comment.Product.Slug, } } return result } func CreateCommentCreatedDto(comment *models.Comment, includes ...bool) map[string]interface{} { return CreateSuccessWithDtoAndMessageDto(GetCommentDetailsDto(comment, includes...), "Comment created successfully") } ================================================ FILE: dtos/orders.go ================================================ package dtos import ( "github.com/melardev/GoGonicEcommerceApi/models" "net/http" ) type CreateOrderRequestDto struct { FirstName string `form:"first_name" json:"first_name" xml:"first_name"` LastName string `form:"last_name" json:"last_name" xml:"last_name"` Country string `form:"country" json:"country" xml:"country"` City string `form:"city" json:"city" xml:"city"` StreetAddress string `form:"street_address" json:"street_address" xml:"street_address" ` ZipCode string `form:"zip_code" json:"zip_code" xml:"zip_code" ` AddressId uint `form:"address_id" json:"address_id" xml:"address_id" ` CartItems []struct { Id uint `form:"id" json:"id" binding:"required"` Quantity int `form:"quantity" json:"quantity" binding:"required"` } `json:"cart_items"` } func CreateOrderPagedResponse(request *http.Request, orders []models.Order, page, page_size, totalOrdersCount int, includes ...bool) map[string]interface{} { var resources = make([]interface{}, len(orders)) for index, order := range orders { includeAddress, includeOrderItems, includeUser := getIncludeFlags(includes...) resources[index] = CreateOrderDto(&order, includeAddress, includeOrderItems, includeUser) } return CreatePagedResponse(request, resources, "orders", page, page_size, totalOrdersCount) } func CreateOrderDto(order *models.Order, includes ...bool) map[string]interface{} { includeAddress, includeOrderItems, includeUser := getIncludeFlags(includes...) result := map[string]interface{}{ "id": order.ID, "tracking_number": order.TrackingNumber, "order_status": order.GetOrderStatusAsString(), } if includeAddress { result["address"] = map[string]interface{}{ "first_name": order.Address.FirstName, "last_name": order.Address.LastName, "street_address": order.Address.StreetAddress, "city": order.Address.City, "country": order.Address.Country, "zip_code": order.Address.ZipCode, } } if includeOrderItems { orderItems := make([]map[string]interface{}, len(order.OrderItems)) for i := 0; i < len(order.OrderItems); i++ { oi := order.OrderItems[i] orderItems[i] = map[string]interface{}{ "name": oi.ProductName, "slug": oi.Slug, "price": oi.Price, } } result["order_items"] = orderItems } else { result["order_items_count"] = order.OrderItemsCount } if includeUser { result["user"] = map[string]interface{}{ "id": order.UserId, "username": order.User.Username, } } return CreateSuccessDto(result) } func CreateOrderDetailsDto(order *models.Order) map[string]interface{} { // includeUser -> false // includeOrderItems -> true // includeUser -> false return CreateSuccessDto(CreateOrderDto(order, true, true, false)) } func getIncludeFlags(includes ...bool) (includeAddress, includeOrderItems, includeUser bool) { if len(includes) > 0 { includeAddress = includes[0] } if len(includes) > 1 { includeOrderItems = includes[1] } if len(includes) > 2 { includeUser = includes[2] } return } func CreateOrderCreatedDto(order *models.Order) map[string]interface{} { return CreateSuccessWithDtoAndMessageDto(CreateOrderDetailsDto(order), "Order created successfully") } ================================================ FILE: dtos/pages.go ================================================ package dtos import "github.com/melardev/GoGonicEcommerceApi/models" func CreateHomeResponse(tags []models.Tag, categories []models.Category) map[string]interface{} { return CreateSuccessDto(map[string]interface{}{ "tags": CreateTagListDto(tags), "categories": CreateCategoryListDto(categories), }) } ================================================ FILE: dtos/products.go ================================================ package dtos import ( "github.com/melardev/GoGonicEcommerceApi/models" "net/http" "strings" "time" ) type ManagedModel models.Product type CreateProduct struct { Name string `form:"name" json:"name" xml:"name" binding:"required"` Description string `form:"description" json:"description" xml:"description" binding:"required"` Price int `form:"price" json:"price" xml:"price" binding:"required"` Stock int `form:"stock" json:"stock" xml:"stock" binding:"required"` } func CreatedProductPagedResponse(request *http.Request, products []models.Product, page, page_size, count int, commentsCount []int) interface{} { var resources = make([]interface{}, len(products)) for index, product := range products { resources[index] = CreateProductDto(&product, commentsCount[index]) } return CreatePagedResponse(request, resources, "products", page, page_size, count) } func CreateProductDto(product *models.Product, commentCount int) map[string]interface{} { var tags = make([]map[string]interface{}, len(product.Tags)) var categories = make([]map[string]interface{}, len(product.Categories)) var images = make([]string, len(product.Images)) for index, tag := range product.Tags { tags[index] = map[string]interface{}{ "id": tag.ID, "name": tag.Name, "slug": tag.Slug, } } for index, category := range product.Categories { categories[index] = map[string]interface{}{ "id": category.ID, "name": category.Name, "slug": category.Slug, } } replaceAllFlag := -1 for index, image := range product.Images { images[index] = strings.Replace(image.FilePath, "\\", "/", replaceAllFlag) } for index, tag := range product.Tags { tags[index] = map[string]interface{}{ "id": tag.ID, "name": tag.Name, "slug": tag.Slug, } } result := map[string]interface{}{ "id": product.ID, "name": product.Name, "slug": product.Slug, "price": product.Price, "stock": product.Stock, "tags": tags, "categories": categories, "image_urls": images, "created_at": product.CreatedAt.UTC().Format("2006-01-02T15:04:05.999Z"), "updated_at": product.UpdatedAt.UTC().Format(time.RFC3339Nano), } if commentCount >= 0 { // "comments_count": product.CommentsCount, result["comments_count"] = commentCount } return result } func CreateProductDetailsDto(product models.Product) map[string]interface{} { result := CreateProductDto(&product, -1) result["description"] = product.Description comments := make([]map[string]interface{}, len(product.Comments)) for index, comment := range product.Comments { comments[index] = GetSummary(&comment, true, false) } result["comments"] = comments return result } func CreateProductCreatedDto(product models.Product) map[string]interface{} { return CreateSuccessWithDtoAndMessageDto(CreateProductDetailsDto(product), "Product crated successfully") } ================================================ FILE: dtos/shared.go ================================================ package dtos import ( "fmt" "github.com/gin-gonic/gin" "gopkg.in/go-playground/validator.v8" "math" "net/http" ) type BaseDto struct { Success bool `json:"success"` FullMessages []string `json:"full_messages"` } type ErrorDto struct { BaseDto Errors map[string]interface{} `json:"errors"` } func CreatePageMeta(request *http.Request, loadedItemsCount, page, page_size, totalItemsCount int) map[string]interface{} { page_meta := map[string]interface{}{} page_meta["offset"] = (page - 1) * page_size page_meta["requested_page_size"] = page_size page_meta["current_page_number"] = page page_meta["current_items_count"] = loadedItemsCount page_meta["prev_page_number"] = 1 total_pages_count := int(math.Ceil(float64(totalItemsCount) / float64(page_size))) page_meta["total_pages_count"] = total_pages_count if page < total_pages_count { page_meta["has_next_page"] = true page_meta["next_page_number"] = page + 1 } else { page_meta["has_next_page"] = false page_meta["next_page_number"] = 1 } if page > 1 { page_meta["prev_page_number"] = page - 1 } else { page_meta["has_prev_page"] = false page_meta["prev_page_number"] = 1 } page_meta["next_page_url"] = fmt.Sprintf("%v?page=%d&page_size=%d", request.URL.Path, page_meta["next_page_number"], page_meta["requested_page_size"]) page_meta["prev_page_url"] = fmt.Sprintf("%s?page=%d&page_size=%d", request.URL.Path, page_meta["prev_page_number"], page_meta["requested_page_size"]) response := gin.H{ "success": true, "page_meta": page_meta, } return response } func CreatePagedResponse(request *http.Request, resources []interface{}, resource_name string, page, page_size, totalItemsCount int) map[string]interface{} { response := CreatePageMeta(request, len(resources), page, page_size, totalItemsCount) response[resource_name] = resources return response } func CreateDetailedErrorDto(key string, err error) map[string]interface{} { return map[string]interface{}{ "success": false, "full_messages": []string{fmt.Sprintf("s -> %v", key, err.Error())}, "errors": err, } } func CreateErrorDtoWithMessage(message string) map[string]interface{} { return map[string]interface{}{ "success": false, "full_messages": []string{message}, } } // This should only be called when we have an Error that is returned from a ShouldBind which contains a lot of information // other kind of errors should use other functions such as CreateDetailedErrorDto func CreateBadRequestErrorDto(err error) ErrorDto { res := ErrorDto{} res.Errors = make(map[string]interface{}) errs := err.(validator.ValidationErrors) res.FullMessages = make([]string, len(errs)) count := 0 for _, v := range errs { if v.ActualTag == "required" { var message = fmt.Sprintf("%v is required", v.Field) res.Errors[v.Field] = message res.FullMessages[count] = message } else { var message = fmt.Sprintf("%v has to be %v", v.Field, v.ActualTag) res.Errors[v.Field] = message res.FullMessages = append(res.FullMessages, message) } count++ } return res } func CreateSuccessDto(result map[string]interface{}) map[string]interface{} { result["success"] = true return result } func CreateSuccessWithMessageDto(message string) interface{} { return CreateSuccessWithMessagesDto([]string{message}) } func CreateSuccessWithMessagesDto(messages []string) interface{} { return gin.H{ "success": true, "full_messages": messages, } } func CreateSuccessWithDtoAndMessagesDto(data map[string]interface{}, messages []string) map[string]interface{} { data["success"] = true data["full_messages"] = messages return data } func CreateSuccessWithDtoAndMessageDto(data map[string]interface{}, message string) map[string]interface{} { return CreateSuccessWithDtoAndMessagesDto(data, []string{message}) } ================================================ FILE: dtos/tags.go ================================================ package dtos import ( "github.com/melardev/GoGonicEcommerceApi/models" "strings" ) type CreateTag struct { Name string `form:"name" binding:"required"` Description string `form:"description" binding:"required"` } func CreateTagListMapDto(tags []models.Tag) map[string]interface{} { result := map[string]interface{}{} var t = make([]interface{}, len(tags)) for i := 0; i < len(tags); i++ { t[i] = CreateTagDto(tags[i]) } result["tags"] = t return CreateSuccessDto(result) } func CreateTagListDto(tags []models.Tag) []interface{} { var t = make([]interface{}, len(tags)) for i := 0; i < len(tags); i++ { t[i] = CreateTagDto(tags[i]) } return t } func CreateTagDto(tag models.Tag) map[string]interface{} { var imageUrls = make([]string, len(tag.Images)) replaceAllFlag := -1 for i := 0; i < len(tag.Images); i++ { imageUrls[i] = strings.Replace(tag.Images[i].FilePath, "\\", "/", replaceAllFlag) } return map[string]interface{}{ "id": tag.ID, "name": tag.Name, "description": tag.Description, "image_urls": imageUrls, } } func CreateTagCreatedDto(tag models.Tag) map[string]interface{} { return CreateSuccessWithDtoAndMessageDto(CreateTagDto(tag), "Tag created successfully") } ================================================ FILE: dtos/users.go ================================================ package dtos import ( "github.com/melardev/GoGonicEcommerceApi/models" ) type RegisterRequestDto struct { Username string `form:"username" json:"username" xml:"username" binding:"required"` FirstName string `form:"first_name" json:"first_name" xml:"first_name" binding:"required"` LastName string `form:"last_name" json:"last_name" xml:"last_name" binding:"required"` Email string `form:"email" json:"email" xml:"email" binding:"required"` Password string `form:"password" json:"password" xml:"password" binding:"required"` PasswordConfirmation string `form:"password_confirmation" json:"password_confirmation" xml:"password-confirmation" binding:"required"` } type LoginRequestDto struct { // Username string `form:"username" json:"username" xml:"username" binding:"exists,username"` Username string `form:"username" json:"username" xml:"username" binding:"required"` Password string `form:"password"json:"password" binding:"exists,min=8,max=255"` userModel models.User `json:"-"` } func CreateLoginSuccessful(user *models.User) map[string]interface{} { var roles = make([]string, len(user.Roles)) for i := 0; i < len(user.Roles); i++ { roles[i] = user.Roles[i].Name } return map[string]interface{}{ "success": true, "token": user.GenerateJwtToken(), "user": map[string]interface{}{ "username": user.Username, "id": user.ID, "roles": roles, }, } } func GetUserBasicInfo(user models.User) map[string]interface{} { return map[string]interface{}{ "id": user.ID, "username": user.Username, } } ================================================ FILE: infrastructure/db.go ================================================ package infrastructure import ( "fmt" "github.com/jinzhu/gorm" "path" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/postgres" _ "github.com/jinzhu/gorm/dialects/sqlite" // import _ "github.com/jinzhu/gorm/dialects/mssql" "os" ) type Database struct { *gorm.DB } var DB *gorm.DB // Opening a database and save the reference to `Database` struct. func OpenDbConnection() *gorm.DB { dialect := os.Getenv("DB_DIALECT") username := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") dbName := os.Getenv("DB_NAME") host := os.Getenv("DB_HOST") var db *gorm.DB var err error if dialect == "sqlite3" { db, err = gorm.Open("sqlite3", path.Join(".", "app.db")) } else { // db, err := gorm.Open("mysql", "root:root@localhost/go_api_shop_gonc?charset=utf8") databaseUrl := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable ", host, username, password, dbName) db, err = gorm.Open(dialect, databaseUrl) } if err != nil { fmt.Println("db err: ", err) os.Exit(-1) } db.DB().SetMaxIdleConns(10) db.LogMode(true) DB = db return DB } // Delete the database after running testing cases. func RemoveDb(db *gorm.DB) error { db.Close() err := os.Remove(path.Join(".", "app.db")) return err } // Using this function to get a connection, you can create your connection pool here. func GetDb() *gorm.DB { return DB } ================================================ FILE: main.go ================================================ package main import ( "fmt" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/joho/godotenv" "github.com/melardev/GoGonicEcommerceApi/controllers" "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/middlewares" "github.com/melardev/GoGonicEcommerceApi/models" "github.com/melardev/GoGonicEcommerceApi/seeds" "os" ) func drop(db *gorm.DB) { db.DropTableIfExists( &models.FileUpload{}, &models.Comment{}, &models.OrderItem{}, &models.Order{}, &models.Address{}, &models.ProductCategory{}, &models.ProductTag{}, &models.Tag{}, &models.Category{}, &models.Product{}, &models.UserRole{}, &models.Role{}, &models.User{}) } func migrate(database *gorm.DB) { database.AutoMigrate(&models.Address{}) database.AutoMigrate(&models.Category{}) database.AutoMigrate(&models.Comment{}) database.AutoMigrate(&models.Order{}) database.AutoMigrate(&models.OrderItem{}) database.AutoMigrate(&models.Product{}) database.AutoMigrate(&models.ProductCategory{}) database.AutoMigrate(&models.Role{}) database.AutoMigrate(&models.UserRole{}) database.AutoMigrate(&models.Tag{}) database.AutoMigrate(&models.ProductTag{}) database.AutoMigrate(&models.User{}) database.AutoMigrate(&models.FileUpload{}) } func addDbConstraints(database *gorm.DB) { // TODO: it is well known GORM does not add foreign keys even after using ForeignKey in struct, but, why manually does not work neither ? dialect := database.Dialect().GetName() // mysql, sqlite3 if dialect != "sqlite3" { database.Model(&models.Comment{}).AddForeignKey("product_id", "products(id)", "CASCADE", "CASCADE") database.Model(&models.Comment{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE") database.Model(&models.Order{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE") database.Model(&models.Order{}).AddForeignKey("address_id", "addresses(id)", "CASCADE", "CASCADE") database.Model(&models.OrderItem{}).AddForeignKey("order_id", "orders(id)", "CASCADE", "CASCADE") database.Model(&models.OrderItem{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE") database.Model(&models.Address{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE") database.Model(&models.UserRole{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE") database.Model(&models.UserRole{}).AddForeignKey("role_id", "roles(id)", "CASCADE", "CASCADE") database.Table("products_tags").AddForeignKey("product_id", "products(id)", "CASCADE", "CASCADE") database.Table("products_tags").AddForeignKey("tag_id", "tags(id)", "CASCADE", "CASCADE") database.Model(models.ProductCategory{}).AddForeignKey("product_id", "products(id)", "CASCADE", "CASCADE") database.Model(models.ProductCategory{}).AddForeignKey("category_id", "categories(id)", "CASCADE", "CASCADE") } else if dialect == "sqlite3" { database.Table("comments").AddIndex("comments__idx_product_id", "product_id") database.Table("comments").AddIndex("comments__idx_user_id", "user_id") database.Table("ratings").AddIndex("ratings__idx_user_id", "user_id") database.Table("ratings").AddIndex("ratings__idx_product_id", "product_id") database.Model(&models.Comment{}).AddIndex("comments__idx_created_at", "created_at") } database.Model(&models.UserRole{}).AddIndex("user_roles__idx_user_id", "user_id") database.Table("products_tags").AddIndex("products_tags__idx_product_id", "product_id") } func create(database *gorm.DB) { drop(database) migrate(database) addDbConstraints(database) } func main() { e := godotenv.Load() //Load .env file if e != nil { fmt.Print(e) } println(os.Getenv("DB_DIALECT")) database := infrastructure.OpenDbConnection() defer database.Close() args := os.Args if len(args) > 1 { first := args[1] second := "" if len(args) > 2 { second = args[2] } if first == "create" { create(database) } else if first == "seed" { seeds.Seed() os.Exit(0) } else if first == "migrate" { migrate(database) } if second == "seed" { seeds.Seed() os.Exit(0) } else if first == "migrate" { migrate(database) } if first != "" && second == "" { os.Exit(0) } } migrate(database) // gin.New() - new gin Instance with no middlewares // goGonicEngine.Use(gin.Logger()) // goGonicEngine.Use(gin.Recovery()) goGonicEngine := gin.Default() // gin with the Logger and Recovery Middlewares attached // Allow all Origins goGonicEngine.Use(cors.Default()) goGonicEngine.Use(middlewares.Benchmark()) // goGonicEngine.Use(middlewares.Cors()) goGonicEngine.Use(middlewares.UserLoaderMiddleware()) goGonicEngine.Static("/static", "./static") apiRouteGroup := goGonicEngine.Group("/api") controllers.RegisterUserRoutes(apiRouteGroup.Group("/users")) controllers.RegisterProductRoutes(apiRouteGroup.Group("/products")) controllers.RegisterCommentRoutes(apiRouteGroup.Group("/")) controllers.RegisterPageRoutes(apiRouteGroup.Group("/")) controllers.RegisterAddressesRoutes(apiRouteGroup.Group("/users")) controllers.RegisterTagRoutes(apiRouteGroup.Group("/tags")) controllers.RegisterCategoryRoutes(apiRouteGroup.Group("/categories")) controllers.RegisterOrderRoutes(apiRouteGroup.Group("/orders")) goGonicEngine.Run(":8080") // listen and serve on 0.0.0.0:8080 } ================================================ FILE: middlewares/auth.go ================================================ package middlewares import ( "fmt" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" "net/http" "os" "strings" ) func EnforceAuthenticatedMiddleware() gin.HandlerFunc { return func(c *gin.Context) { user, exists := c.Get("currentUser") if exists && user.(models.User).ID != 0 { return } else { err, _ := c.Get("authErr") _ = c.AbortWithError(http.StatusUnauthorized, err.(error)) return } } } func UserLoaderMiddleware() gin.HandlerFunc { return func(c *gin.Context) { bearer := c.Request.Header.Get("Authorization") if bearer != "" { jwtParts := strings.Split(bearer, " ") if len(jwtParts) == 2 { jwtEncoded := jwtParts[1] token, err := jwt.Parse(jwtEncoded, func(token *jwt.Token) (interface{}, error) { // Theorically we have also to validate the algorithm if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signin method %v", token.Header["alg"]) } secret := []byte(os.Getenv("JWT_SECRET")) return secret, nil }) if err != nil { println(err.Error()) return } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { userId := uint(claims["user_id"].(float64)) fmt.Printf("[+] Authenticated request, authenticated user id is %d\n", userId) var user models.User if userId != 0 { database := infrastructure.GetDb() // We always need the Roles to be loaded to make authorization decisions based on Roles database.Preload("Roles").First(&user, userId) } c.Set("currentUser", user) c.Set("currentUserId", user.ID) } else { } } } } } ================================================ FILE: middlewares/benchmark.go ================================================ package middlewares import ( "fmt" "github.com/gin-gonic/gin" "math" "time" ) func Benchmark() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() elapsed := time.Since(start) fmt.Printf("Request took %v milliseconds\n", float64(elapsed.Nanoseconds())/math.Pow(float64(10), float64(6))) } } ================================================ FILE: middlewares/cors.go ================================================ package middlewares import ( "github.com/gin-gonic/gin" ) func Cors() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type,Token") c.Next() } } ================================================ FILE: models/address.go ================================================ package models import "github.com/jinzhu/gorm" type Address struct { gorm.Model StreetAddress string `gorm:"not null"` City string `gorm:"not null"` Country string `gorm:"not null"` ZipCode string `gorm:"not null"` FirstName string `gorm:"not null"` LastName string `gorm:"not null"` User User `gorm:"association_foreignkey:UserId:"` UserId uint `gorm:"default:null"` // Guest users may place an order, so they should be able to create an address with nullable UserId Orders []Order `gorm:"foreignKey:AddressId"` } ================================================ FILE: models/category.go ================================================ package models import ( "github.com/gosimple/slug" "github.com/jinzhu/gorm" ) type Category struct { gorm.Model Name string `gorm:"not null"` Description string `gorm:"default:null"` Slug string `gorm:"unique_index"` Products []Product `gorm:"many2many:products_categories;"` Images []FileUpload `gorm:"foreignKey:CategoryId"` IsNewRecord bool `gorm:"-;default:false"` } func (a *Category) BeforeSave() (err error) { a.Slug = slug.Make(a.Name) return } ================================================ FILE: models/comment.go ================================================ package models import ( "github.com/jinzhu/gorm" ) type Comment struct { gorm.Model Content string `gorm:"size:2048"` Rating int `gorm:"default:null"` Product Product `gorm:"foreignkey:ProductId"` ProductId uint `gorm:"not null"` User User `gorm:"foreignkey:UserId"` UserId uint `gorm:"not null"` } ================================================ FILE: models/file_upload.go ================================================ package models import "github.com/jinzhu/gorm" type FileUpload struct { gorm.Model Filename string FilePath string OriginalName string FileSize uint Tag Tag `gorm:"association_foreignkey:TagId"` TagId uint `gorm:"default:null"` Category Category `gorm:"association_foreignkey:CategoryId"` CategoryId uint `gorm:"default:null"` Product Category `gorm:"association_foreignkey:ProductId"` ProductId uint `gorm:"default:null"` } // Scopes, not used func TagImages(db *gorm.DB) *gorm.DB { return db.Where("type = ?", "TagImage") } func CategoryImages(db *gorm.DB) *gorm.DB { return db.Where("type = ?", "CategoryImage") } func ProductImages(db *gorm.DB) *gorm.DB { return db.Where("type = ?", "ProductImage") } // db.Scopes(CategoryImages, ProductImages).Find(&images) ================================================ FILE: models/order.go ================================================ package models import "github.com/jinzhu/gorm" type Order struct { gorm.Model OrderStatus int `gorm:"default:0"` TrackingNumber string OrderItems []OrderItem `gorm:"foreignKey:OrderId"` Address Address `gorm:"association_foreignkey:AddressId:"` AddressId uint User User `gorm:"foreignKey:UserId:"` UserId uint `gorm:"default:null"` OrderItemsCount int `gorm:"-"` } func (order *Order) GetOrderStatusAsString() string { switch order.OrderStatus { case 0: return "processed" case 1: return "delivered" case 2: return "shipped" default: return "unknown" } } ================================================ FILE: models/order_item.go ================================================ package models import "github.com/jinzhu/gorm" type OrderItem struct { gorm.Model Order Order OrderId uint `gorm:"not null"` Product Product ProductId uint `gorm:"not null"` Slug string `gorm:"not null"` ProductName string `gorm:"not null"` Price int `gorm:"not null"` Quantity int `gorm:"not null"` User User `gorm:"association_foreignkey:UserId:"` UserId uint `gorm:"default:null"` } ================================================ FILE: models/product.go ================================================ package models import ( "github.com/gosimple/slug" "github.com/jinzhu/gorm" ) type Product struct { gorm.Model Name string `gorm:"size:280;not null"` Description string `gorm:"not null"` Slug string `gorm:"unique_index;not null"` Price int `gorm:"not null"` Stock int `gorm:"not null"` Tags []Tag `gorm:"many2many:products_tags;"` ProductTags []ProductTag `gorm:"foreignkey:ProductId"` Categories []Category `gorm:"many2many:products_categories;"` ProductCategories []ProductCategory `gorm:"foreignkey:ProductId"` Comments []Comment `gorm:"foreignKey:ProductId"` Images []FileUpload `gorm:"foreignKey:ProductId"` CommentsCount int `gorm:"-"` } func (product *Product) BeforeSave() (err error) { product.Slug = slug.Make(product.Name) return } ================================================ FILE: models/product_category.go ================================================ package models type ProductCategory struct { Category User `gorm:"association_foreignkey:CategoryId"` CategoryId uint Product Product `gorm:"association_foreignkey:ProductId"` ProductId uint } func (*ProductCategory) TableName() string { return "products_categories" } ================================================ FILE: models/product_tag.go ================================================ package models type ProductTag struct { Tag User `gorm:"association_foreignkey:TagId"` TagId uint Product Product `gorm:"association_foreignkey:ProductId"` ProductId uint } func (*ProductTag) TableName() string { return "products_tags" } ================================================ FILE: models/role.go ================================================ package models import "github.com/jinzhu/gorm" type Role struct { gorm.Model Name string Description string Users []User `gorm:"many2many:users_roles;"` UserRoles []UserRole `gorm:"foreignkey:RoleId"` } type UserRole struct { User User `gorm:"association_foreignkey:UserId"` UserId uint Role User `gorm:"association_foreignkey:RoleId"` RoleId uint } func (UserRole) TableName() string { return "users_roles" } func Any(roles []Role, f func(Role) bool) bool { for _, role := range roles { if f(role) { return true } } return false } ================================================ FILE: models/tag.go ================================================ package models import ( "github.com/gosimple/slug" "github.com/jinzhu/gorm" ) type Tag struct { gorm.Model Name string `gorm:"not null"` Description string `gorm:"default:null"` Slug string `gorm:"unique_index"` Products []Product `gorm:"many2many:products_tags;"` Images []FileUpload `gorm:"foreignKey:TagId"` IsNewRecord bool `gorm:"-;default:false"` // Virtual Field, so it is not persisted in the Db. This is used in FirstOrCreate() } func (a *Tag) BeforeSave() (err error) { a.Slug = slug.Make(a.Name) return } ================================================ FILE: models/user.go ================================================ package models import ( "errors" "github.com/dgrijalva/jwt-go" "github.com/jinzhu/gorm" "golang.org/x/crypto/bcrypt" "os" "time" ) type User struct { gorm.Model //Id uint `gorm:"primary_key"` FirstName string `gorm:"varchar(255);not null"` LastName string `gorm:"varchar(255);not null"` Username string `gorm:"column:username"` Email string `gorm:"column:email;unique_index"` Password string `gorm:"column:password;not null"` Comments []Comment `gorm:"foreignkey:UserId"` Roles []Role `gorm:"many2many:users_roles;"` UserRoles []UserRole `gorm:"foreignkey:UserId"` } // What's bcrypt? https://en.wikipedia.org/wiki/Bcrypt // Golang bcrypt doc: https://godoc.org/golang.org/x/crypto/bcrypt // You can change the value in bcrypt.DefaultCost to adjust the security index. // err := userModel.setPassword("password0") func (u *User) SetPassword(password string) error { if len(password) == 0 { return errors.New("password should not be empty") } bytePassword := []byte(password) // Make sure the second param `bcrypt generator cost` between [4, 32) passwordHash, _ := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost) u.Password = string(passwordHash) return nil } // Database will only save the hashed string, you should check it by util function. // if err := serModel.checkPassword("password0"); err != nil { password error } func (u *User) IsValidPassword(password string) error { bytePassword := []byte(password) byteHashedPassword := []byte(u.Password) return bcrypt.CompareHashAndPassword(byteHashedPassword, bytePassword) } func (user *User) BeforeSave(db *gorm.DB) (err error) { if len(user.Roles) == 0 { // role := Role{} userRole := Role{} // db.Model(&role).Where("name = ?", "ROLE_USER").First(&userRole) db.Model(&Role{}).Where("name = ?", "ROLE_USER").First(&userRole) //db.Where(&models.Role{Name: "ROLE_USER"}).Attrs(models.Role{Description: "For standard Users"}).FirstOrCreate(&userRole) user.Roles = append(user.Roles, userRole) } return } // Generate JWT token associated to this user func (user *User) GenerateJwtToken() string { // jwt.New(jwt.GetSigningMethod("HS512")) jwt_token := jwt.New(jwt.SigningMethodHS512) var roles []string for _, role := range user.Roles { roles = append(roles, role.Name) } jwt_token.Claims = jwt.MapClaims{ "user_id": user.ID, "username": user.Username, "roles": roles, "exp": time.Now().Add(time.Hour * 24 * 90).Unix(), } // Sign and get the complete encoded token as a string token, _ := jwt_token.SignedString([]byte(os.Getenv("JWT_SECRET"))) return token } func (user *User) IsAdmin() bool { for _, role := range user.Roles { if role.Name == "ROLE_ADMIN" { return true } } return false } func (user *User) IsNotAdmin() bool { return !user.IsAdmin() } ================================================ FILE: seeds/seeder.go ================================================ package seeds import ( "github.com/icrowley/fake" "github.com/jinzhu/gorm" "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" "golang.org/x/crypto/bcrypt" "math/rand" "time" ) func randomInt(min, max int) int { return rand.Intn(max-min) + min } var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randomString(length int) string { b := make([]rune, length) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) } func seedAdmin(db *gorm.DB) { count := 0 adminRole := models.Role{Name: "ROLE_ADMIN", Description: "Only for admin"} query := db.Model(&models.Role{}).Where("name = ?", "ROLE_ADMIN") query.Count(&count) if count == 0 { db.Create(&adminRole) } else { query.First(&adminRole) } adminRoleUsers := 0 var adminUsers []models.User db.Model(&adminRole).Related(&adminUsers, "Users") db.Model(&models.User{}).Where("username = ?", "admin").Count(&adminRoleUsers) if adminRoleUsers == 0 { // query.First(&adminRole) // First would fetch the Role admin because the query status name='ROLE_ADMIN' password, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost) // Approach 1 user := models.User{FirstName: "AdminFN", LastName: "AdminFN", Email: "admin@golang.com", Username: "admin", Password: string(password)} user.Roles = append(user.Roles, adminRole) // Do not try to update the adminRole db.Set("gorm:association_autoupdate", false).Create(&user) // Approach 2 // user := models.User{FirstName: "AdminFN", LastName: "AdminFN", Email: "admin@golang.com", Username: "admin", Password: "password"} // user.Roles = append(user.Roles, adminRole) // db.NewRecord(user) // db.Set("gorm:association_autoupdate", false).Save(&user) if db.Error != nil { print(db.Error) } } } func seedUsers(db *gorm.DB) { count := 0 role := models.Role{Name: "ROLE_USER", Description: "Only for standard users"} q := db.Model(&models.Role{}).Where("name = ?", "ROLE_USER") q.Count(&count) if count == 0 { db.Create(&role) } else { q.First(&role) } var standardUsers []models.User db.Model(&role).Related(&standardUsers, "Users") usersCount := len(standardUsers) usersToSeed := 20 usersToSeed -= usersCount if usersToSeed > 0 { for i := 0; i < usersToSeed; i++ { password, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost) user := models.User{FirstName: fake.FirstName(), LastName: fake.LastName(), Email: fake.EmailAddress(), Username: fake.UserName(), Password: string(password)} // No need to add the role as we did for seedAdmin, it is added by the BeforeSave hook db.Set("gorm:association_autoupdate", false).Create(&user) } } } func seedTags(db *gorm.DB) { var tags [3]models.Tag db.Where(&models.Tag{Name: "Shoes"}).Attrs(models.Tag{Description: "Shoes for everyone", IsNewRecord: true}).FirstOrCreate(&tags[0]) db.Where(models.Tag{Name: "Jackets"}).Attrs(models.Tag{Description: "Jackets for everyone", IsNewRecord: true}).FirstOrCreate(&tags[1]) db.Where(models.Tag{Name: "Jeans"}).Attrs(models.Tag{Description: "Jeans for everyone", IsNewRecord: true}).FirstOrCreate(&tags[2]) for _, tag := range tags { for i := 0; i < randomInt(1, 3); i++ { if tag.IsNewRecord { db.Create(&models.FileUpload{Filename: randomString(16) + ".png", OriginalName: randomString(16) + ".png", FilePath: "/static/images/tags/" + randomString(16) + ".png", FileSize: 2500, Tag: tag, TagId: tag.ID}) } } } } func seedCategories(db *gorm.DB) { var categories [3]models.Category db.Where(models.Category{Name: "Women"}).Attrs(models.Category{Description: "Clothes for women", IsNewRecord: true}).FirstOrCreate(&categories[0]) db.Where(models.Category{Name: "Men"}).Attrs(models.Category{Description: "Clothes for men", IsNewRecord: true}).FirstOrCreate(&categories[1]) db.Where(models.Category{Name: "Kids"}).Attrs(models.Category{Description: "Clothes for kids", IsNewRecord: true}).FirstOrCreate(&categories[2]) for _, category := range categories { for i := 0; i < randomInt(1, 3); i++ { if category.IsNewRecord { db.Create(&models.FileUpload{Filename: randomString(16) + ".png", OriginalName: randomString(16) + ".png", FilePath: "/static/images/categories/" + randomString(16) + ".png", FileSize: 2500, Category: category, CategoryId: category.ID}) } } } } func seedProducts(db *gorm.DB) { productsCount := 0 productsToSeed := 20 db.Model(&models.Product{}).Count(&productsCount) productsToSeed -= productsCount if productsToSeed > 0 { rand.Seed(time.Now().Unix()) tags := []models.Tag{} categories := []models.Category{} db.Find(&tags) db.Find(&categories) for i := 0; i < productsToSeed; i++ { // add a tag and a category for each product // faker.RandomInt(0, len(tags))[0] tagForProduct := tags[rand.Intn(len(tags))] categoryForProduct := categories[rand.Intn(len(categories))] product := &models.Product{Name: fake.ProductName(), Description: fake.Paragraph(), Stock: randomInt(100, 2000), Price: randomInt(50, 1000), Tags: []models.Tag{tagForProduct}, Categories: []models.Category{categoryForProduct}} for i := 0; i < randomInt(1, 4); i++ { productImage := models.FileUpload{Filename: randomString(16) + ".png", OriginalName: randomString(16) + ".png", FilePath: "/static/images/products/" + randomString(16) + ".png", FileSize: uint(randomInt(1000, 23000))} product.Images = append(product.Images, productImage) db.Set("gorm:association_autoupdate", false).Create(&product) } /* db.Create(&models.FileUpload{Filename: randomString(16) + ".png", OriginalName: randomString(16) + ".png", FilePath: "/static/images/tags" + randomString(16) + ".png", FileSize: 2500, Tag: tag, TagId: tag.ID}) */ } } } func seedComments(db *gorm.DB) { commentsCount := 0 commentsToSeed := 20 allUsers := []models.User{} allProducts := []models.Product{} db.Model(&models.Comment{}).Count(&commentsCount) commentsToSeed -= commentsCount if commentsToSeed > 0 { rand.Seed(time.Now().Unix()) db.Find(&allProducts) db.Find(&allUsers) for i := 0; i < commentsToSeed; i++ { userId := allUsers[rand.Intn(len(allUsers))].ID productId := allProducts[rand.Intn(len(allProducts))].ID sentences := fake.SentencesN(randomInt(2, 6)) var comment models.Comment if rand.Float32() > 0.3 { comment = models.Comment{Content: sentences, UserId: userId, ProductId: productId} } else { // Comment with rating comment = models.Comment{Content: sentences, UserId: userId, ProductId: productId, Rating: randomInt(1, 5)} } db.Set("gorm:association_autoupdate", false).Create(&comment) } } } func seedAddresses(db *gorm.DB) { addressesCount := 0 addressesToSeed := 20 allUsers := []models.User{} db.Model(&models.Address{}).Count(&addressesCount) addressesToSeed -= addressesCount if addressesToSeed > 0 { rand.Seed(time.Now().Unix()) db.Find(&allUsers) var address models.Address var city string var country string var streetAddress string var zipCode string for i := 0; i < addressesToSeed; i++ { city = fake.City() country = fake.Country() zipCode = fake.Zip() streetAddress = fake.StreetAddress() address = models.Address{ZipCode: zipCode, StreetAddress: streetAddress, Country: country, City: city} if rand.Float32() > 0.4 { user := allUsers[rand.Intn(len(allUsers))] address.UserId = user.ID address.FirstName = user.FirstName address.LastName = user.LastName } else { address.FirstName = fake.FirstName() address.LastName = fake.LastName() } db.Set("gorm:association_autoupdate", false).Create(&address) } } } func seedOrders(db *gorm.DB) { ordersCount := 0 ordersToSeed := 20 allAddresses := []models.Address{} allProducts := []models.Product{} db.Model(&models.Order{}).Count(&ordersCount) ordersToSeed -= ordersCount if ordersToSeed > 0 { rand.Seed(time.Now().Unix()) // Eager load the address's user association db.Find(&allAddresses) db.Find(&allProducts) for i := 0; i < ordersToSeed; i++ { address := allAddresses[rand.Intn(len(allAddresses))] order := models.Order{TrackingNumber: randomString(16), OrderStatus: randomInt(0, 3), AddressId: address.ID} orderItemsForOrder := randomInt(2, 5) if rand.Float32() > 0.3 { order.UserId = address.UserId } for j := 0; j < orderItemsForOrder; j++ { product := allProducts[rand.Intn(len(allProducts))] orderItem := models.OrderItem{ProductName: product.Name, Price: product.Price, Slug: product.Slug, ProductId: product.ID, UserId: address.UserId, Quantity: randomInt(1, 8)} order.OrderItems = append(order.OrderItems, orderItem) } db.Set("gorm:association_autoupdate", false).Create(&order) } } } func Seed() { db := infrastructure.GetDb() rand.Seed(time.Now().UnixNano()) seedAdmin(db) seedUsers(db) seedTags(db) seedCategories(db) seedProducts(db) seedComments(db) seedAddresses(db) seedOrders(db) } ================================================ FILE: services/addresses.go ================================================ package services import ( "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" ) func FetchAddressesPage(userId uint, page, pageSize int, includeUser bool) ([]models.Address, int) { var addresses []models.Address var totalAddressesCount int database := infrastructure.GetDb() database.Model(&models.Address{}).Where(&models.Address{UserId: uint(userId)}).Count(&totalAddressesCount) database.Where(&models.Address{UserId: uint(userId)}). Offset((page - 1) * pageSize).Limit(pageSize). Preload("User"). Find(&addresses) if includeUser { var userIds = make([]uint, len(addresses)) var users []models.User for i := 0; i < len(addresses); i++ { userIds[i] = addresses[i].UserId } database.Select([]string{"id", "username"}).Where(userIds).Find(&users) // If the user gets deleted and the comment is still in the database we may have less users than addresses // Another scenario (the one I run into) is there is a problem with the Comment.User, the Comment.UserId does not get saved automatically for i := 0; i < len(addresses); i++ { address := addresses[i] for j := 0; j < len(users); j++ { user := users[j] if address.UserId == user.ID { addresses[i].User = users[j] } } } } return addresses, totalAddressesCount } func FetchAddress(addressId uint) (address models.Address) { database := infrastructure.GetDb() database.First(&address, addressId) return address } func FetchIdsFromAddress(addressId uint) (address models.Address) { database := infrastructure.GetDb() database.Select("id, user_id").First(&address, addressId) return } ================================================ FILE: services/categories.go ================================================ package services import ( "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" ) func FetchAllCategories() ([]models.Category, error) { database := infrastructure.GetDb() var categories []models.Category err := database.Preload("Images", "category_id IS NOT NULL").Find(&categories).Error return categories, err } ================================================ FILE: services/comments.go ================================================ package services import ( "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" ) func FetchCommentsPage(productId, page int, page_size int) ([]models.Comment, int) { // TODO: Why Preload does not load the User? the error is can't preload field User for models.Comment var comments []models.Comment var totalCommentCount int database := infrastructure.GetDb() database.Model(&comments).Where(&models.Comment{ProductId: uint(productId)}).Count(&totalCommentCount) database.Where(&models.Comment{ProductId: uint(productId)}). Offset((page - 1) * page_size).Limit(page_size). Preload("User"). Find(&comments) // `Where in` using other columns different than ID // database.Where("username in (?)", []string{"admin", "melardev"}).Find(&users) var userIds = make([]uint, len(comments)) var users []models.User for i := 0; i < len(comments); i++ { userIds[i] = comments[i].UserId } database.Select("id, username").Where(userIds).Find(&users) // If the user gets deleted and the comment is still in the database we may have less users than comments // Another scenario (the one I run into) is there is a problem with the Comment.User, the Comment.UserId does not get saved automatically for i := 0; i < len(comments); i++ { comment := comments[i] for j := 0; j < len(users); j++ { user := users[j] if comment.UserId == user.ID { comments[i].User = users[j] } } } return comments, totalCommentCount } func FetchCommentById(id int, includes ...bool) models.Comment { includeUser := false if len(includes) > 0 { includeUser = includes[0] } includeProduct := false if len(includes) > 1 { includeProduct = includes[1] } database := infrastructure.GetDb() var comment models.Comment if includeProduct && includeUser { database.Preload("User").Preload("Product").Find(&comment, id) } else if includeUser { database.Preload("User").Find(&comment, id) } else if includeProduct { database.Preload("Product").Find(&comment, id) } else { database.Find(&comment, id) } return comment } func DeleteComment(condition interface{}) error { database := infrastructure.GetDb() err := database.Where(condition).Delete(models.Comment{}).Error return err } ================================================ FILE: services/orders.go ================================================ package services import ( "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" ) func FetchOrdersPage(userId uint, page, pageSize int) (orders []models.Order, totalOrdersCount int, err error) { database := infrastructure.GetDb() totalOrdersCount = 0 query := database.Model(&models.Order{}).Where(&models.Order{UserId: userId}) query.Count(&totalOrdersCount) err = query.Offset((page - 1) * pageSize).Limit(pageSize). // TODO: Why Preload("Address") does not work?, perhaps OrderItems does // Preload("OrderItems").Preload("Address"). Find(&orders).Error if err != nil { return } var orderIds = make([]uint, len(orders)) for i := 0; i < len(orders); i++ { orderIds[i] = orders[i].ID } var orderItems []models.OrderItem if len(orders) > 0 { // database.Select("id, order_id").Where("order_id in (?)", orderIds).Find(&orderItems) for i := 0; i < len(orderItems); i++ { oi := orderItems[i] for j := 0; j < len(orders); j++ { if oi.OrderId == orders[j].ID { orders[j].OrderItemsCount = orders[j].OrderItemsCount + 1 } } } } return orders, totalOrdersCount, err } func FetchOrderDetails(orderId uint) (order models.Order, err error) { database := infrastructure.GetDb() err = database.Model(models.Order{}).Preload("OrderItems").First(&order, orderId).Error var address models.Address database.Model(&order).Related(&address) order.Address = address return order, err } ================================================ FILE: services/products.go ================================================ package services import ( "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" ) func FetchProductsPage(page int, page_size int) ([]models.Product, int, []int, error) { database := infrastructure.GetDb() var products []models.Product var count int tx := database.Begin() database.Model(&products).Count(&count) database.Offset((page - 1) * page_size).Limit(page_size).Find(&products) tx.Model(&products). Preload("Tags").Preload("Categories").Preload("Images"). Order("created_at desc").Offset((page - 1) * page_size).Limit(page_size).Find(&products) commentsCount := make([]int, len(products)) for index, product := range products { commentsCount[index] = tx.Model(&product).Association("Comments").Count() } err := tx.Commit().Error return products, count, commentsCount, err } func FetchProductDetails(condition interface{}, optional ...bool) models.Product { database := infrastructure.GetDb() var product models.Product query := database.Where(condition). Preload("Tags").Preload("Categories").Preload("Images").Preload("Comments") // Unfortunately .Preload("Comments.User") does not work as the doc states ... query.First(&product) includeUserComment := false if len(optional) > 0 { includeUserComment = optional[0] } if includeUserComment { for i := 0; i < len(product.Comments); i++ { database.Model(&product.Comments[i]).Related(&product.Comments[i].User, "UserId") } var userIds = make([]uint, len(product.Comments)) var users []models.User for i := 0; i < len(product.Comments); i++ { userIds[i] = product.Comments[i].UserId } // WHERE users.id IN userIds; This will also work: Select([]string{"id", "username"}) database.Select("id, username").Where(userIds).Find(&users) for i := 0; i < len(product.Comments); i++ { user := users[i] comment := product.Comments[i] if comment.UserId == user.ID { product.Comments[i].User = users[i] } } } return product } func FetchProductId(slug string) (uint, error) { productId := -1 database := infrastructure.GetDb() err := database.Model(&models.Product{}).Where(&models.Product{Slug: slug}).Select("id").Row().Scan(&productId) return uint(productId), err } func SetTags(product *models.Product, tags []string) error { database := infrastructure.GetDb() var tagList []models.Tag for _, tag := range tags { var tagModel models.Tag err := database.FirstOrCreate(&tagModel, models.Tag{Name: tag}).Error if err != nil { return err } tagList = append(tagList, tagModel) } product.Tags = tagList return nil } func Update(product *models.Product, data interface{}) error { database := infrastructure.GetDb() err := database.Model(product).Update(data).Error return err } func DeleteProduct(condition interface{}) error { db := infrastructure.GetDb() err := db.Where(condition).Delete(models.Product{}).Error return err } func FetchProductsIdNameAndPrice(productIds []uint) (products []models.Product, err error) { database := infrastructure.GetDb() err = database.Select([]string{"id", "name", "slug", "price"}).Find(&products, productIds).Error return products, err } ================================================ FILE: services/shared.go ================================================ package services import ( "github.com/melardev/GoGonicEcommerceApi/infrastructure" ) func CreateOne(data interface{}) error { database := infrastructure.GetDb() err := database.Create(data).Error return err } func SaveOne(data interface{}) error { database := infrastructure.GetDb() err := database.Save(data).Error return err } ================================================ FILE: services/tags.go ================================================ package services import ( "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" ) func FetchAllTags() ([]models.Tag, error) { database := infrastructure.GetDb() var tags []models.Tag err := database.Preload("Images", "tag_id IS NOT NULL").Find(&tags).Error return tags, err } ================================================ FILE: services/users.go ================================================ package services import ( "github.com/melardev/GoGonicEcommerceApi/infrastructure" "github.com/melardev/GoGonicEcommerceApi/models" ) // You could input the conditions and it will return an User in database with error info. // userModel, err := FindOneUser(&User{Username: "username0"}) func FindOneUser(condition interface{}) (models.User, error) { database := infrastructure.GetDb() var user models.User err := database.Where(condition).Preload("Roles").First(&user).Error return user, err } // You could update properties of an User to database returning with error info. // err := db.Model(userModel).Update(User{Username: "wangzitian0"}).Error func UpdateUser(user models.User, data interface{}) error { database := infrastructure.GetDb() err := database.Model(user).Update(data).Error return err }