├── .env ├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── api ├── api.go └── v1.0 │ ├── auth │ ├── auth.ctrl.go │ └── auth.go │ ├── posts │ ├── posts.ctrl.go │ └── posts.go │ └── v1.0.go ├── database ├── database.go ├── inject.go └── models │ ├── migrate.go │ ├── post.go │ └── user.go ├── jwtsecret.key ├── lib ├── common │ └── common.go └── middlewares │ ├── authorized.go │ └── jwt.go ├── main.go └── scripts └── start-dev /.env: -------------------------------------------------------------------------------- 1 | PORT=4000 2 | DB_CONFIG=sample:samplepass@/sample?charset=utf8&parseTime=True&loc=Local -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | gin-bin 3 | main -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/dgrijalva/jwt-go" 6 | packages = ["."] 7 | revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" 8 | version = "v3.2.0" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/gin-contrib/sse" 13 | packages = ["."] 14 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae" 15 | 16 | [[projects]] 17 | name = "github.com/gin-gonic/gin" 18 | packages = [ 19 | ".", 20 | "binding", 21 | "render" 22 | ] 23 | revision = "d459835d2b077e44f7c9b453505ee29881d5d12d" 24 | version = "v1.2" 25 | 26 | [[projects]] 27 | name = "github.com/go-sql-driver/mysql" 28 | packages = ["."] 29 | revision = "d523deb1b23d913de5bdada721a6071e71283618" 30 | version = "v1.4.0" 31 | 32 | [[projects]] 33 | name = "github.com/golang/protobuf" 34 | packages = ["proto"] 35 | revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" 36 | version = "v1.1.0" 37 | 38 | [[projects]] 39 | name = "github.com/jinzhu/gorm" 40 | packages = [ 41 | ".", 42 | "dialects/mysql" 43 | ] 44 | revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166" 45 | version = "v1.9.1" 46 | 47 | [[projects]] 48 | branch = "master" 49 | name = "github.com/jinzhu/inflection" 50 | packages = ["."] 51 | revision = "04140366298a54a039076d798123ffa108fff46c" 52 | 53 | [[projects]] 54 | name = "github.com/joho/godotenv" 55 | packages = ["."] 56 | revision = "a79fa1e548e2c689c241d10173efd51e5d689d5b" 57 | version = "v1.2.0" 58 | 59 | [[projects]] 60 | name = "github.com/mattn/go-isatty" 61 | packages = ["."] 62 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 63 | version = "v0.0.3" 64 | 65 | [[projects]] 66 | name = "github.com/ugorji/go" 67 | packages = ["codec"] 68 | revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab" 69 | version = "v1.1.1" 70 | 71 | [[projects]] 72 | branch = "master" 73 | name = "golang.org/x/crypto" 74 | packages = [ 75 | "bcrypt", 76 | "blowfish" 77 | ] 78 | revision = "c126467f60eb25f8f27e5a981f32a87e3965053f" 79 | 80 | [[projects]] 81 | branch = "master" 82 | name = "golang.org/x/sys" 83 | packages = ["unix"] 84 | revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4" 85 | 86 | [[projects]] 87 | name = "google.golang.org/appengine" 88 | packages = ["cloudsql"] 89 | revision = "b1f26356af11148e710935ed1ac8a7f5702c7612" 90 | version = "v1.1.0" 91 | 92 | [[projects]] 93 | name = "gopkg.in/go-playground/validator.v8" 94 | packages = ["."] 95 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" 96 | version = "v8.18.2" 97 | 98 | [[projects]] 99 | name = "gopkg.in/yaml.v2" 100 | packages = ["."] 101 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 102 | version = "v2.2.1" 103 | 104 | [solve-meta] 105 | analyzer-name = "dep" 106 | analyzer-version = 1 107 | inputs-digest = "e931b15a1fd782e0b847fa350595969a9b2340ef41ebd231a74b27cd3e65fef0" 108 | solver-name = "gps-cdcl" 109 | solver-version = 1 110 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [prune] 29 | go-tests = true 30 | unused-packages = true 31 | [[constraint]] 32 | name = "github.com/gin-gonic/gin" 33 | version = "1.2.0" 34 | 35 | [[constraint]] 36 | name = "github.com/joho/godotenv" 37 | version = "1.2.0" 38 | 39 | [[constraint]] 40 | name = "github.com/jinzhu/gorm" 41 | version = "1.9.1" 42 | 43 | [[constraint]] 44 | name = "github.com/dgrijalva/jwt-go" 45 | version = "3.2.0" 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Minjun Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## gin-rest-api-sample 2 | Golang REST API sample with MariaDB integration using Gin and GORM. (This project IS NOT a starter kit, it is just an example project.) 3 | 4 | This project is a sample project that contains following features: 5 | 6 | - REST API server with [Gin Framework](https://github.com/gin-gonic/gin) 7 | - Modular Routes 8 | - Database integration using [GORM](http://gorm.io/) 9 | - Live Reload using [codegangsta/gin](https://github.com/codegangsta/gin) 10 | - JWT Token based Authentication 11 | - [Supported REST API Documentation](https://documenter.getpostman.com/view/723994/RWTeVNA4) (Postman) 12 | 13 | 14 | ## Project Setup 15 | 16 | ``` 17 | $ dep ensure 18 | $ go get github.com/jinzhu/gorm 19 | $ go get github.com/codegangsta/gin 20 | ``` 21 | 22 | GORM should be installed via `go get` since installation via `dep` is imperfect (it does not download dialects directory). 23 | 24 | [codegangsta/gin](https://github.com/codegangsta/gin) is an optional package to install if you want to make usage of live reloading feature of server (just like nodemon in Node.js environment). 25 | 26 | ### MariaDB Configuration 27 | 28 | This project uses MariaDB to store data. Install MariaDB and create a sample database and a user account. 29 | 30 | #### Install MariaDB 31 | 32 | - [macOS](https://mariadb.com/kb/en/library/installing-mariadb-on-macos-using-homebrew/) 33 | - [Windows](https://mariadb.com/kb/en/library/installing-mariadb-msi-packages-on-windows/) 34 | - [Ubuntu](https://www.itzgeek.com/how-tos/linux/ubuntu-how-tos/install-mariadb-on-ubuntu-16-04.html) 35 | 36 | 37 | #### Create Database / Account 38 | ```sql 39 | CREATE DATABASE sample; 40 | GRANT ALL PRIVILEGES ON sample.* to sample@'%' IDENTIFIED BY 'samplepass'; 41 | GRANT ALL PRIVILEGES ON sample.* to sample@'localhost' IDENTIFIED BY 'samplepass'; 42 | ``` 43 | 44 | ### Configure Environment Variables 45 | 46 | Open .env file and edit the values if you need to. This project uses [godotenv](https://github.com/joho/godotenv) to read and use .env file. 47 | 48 | Database config string is formatted in [go-sql-driver format](https://github.com/go-sql-driver/mysql#parameters). 49 | 50 | ## Start Project 51 | 52 | ``` 53 | $ go run main.go 54 | ``` 55 | 56 | To explicitly compile the code before you run the server: 57 | 58 | ``` 59 | $ go build main.go 60 | $ ./main 61 | ``` 62 | 63 | To use live-reloading in development environment, 64 | 65 | ``` 66 | $ ./scripts/start-dev 67 | ``` -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/velopert/gin-rest-api-sample/api/v1.0" 6 | ) 7 | 8 | // ApplyRoutes applies router to gin Router 9 | func ApplyRoutes(r *gin.Engine) { 10 | api := r.Group("/api") 11 | { 12 | apiv1.ApplyRoutes(api) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/v1.0/auth/auth.ctrl.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "time" 8 | 9 | jwt "github.com/dgrijalva/jwt-go" 10 | "github.com/gin-gonic/gin" 11 | "github.com/jinzhu/gorm" 12 | "github.com/velopert/gin-rest-api-sample/database/models" 13 | "github.com/velopert/gin-rest-api-sample/lib/common" 14 | "golang.org/x/crypto/bcrypt" 15 | ) 16 | 17 | // User is alias for models.User 18 | type User = models.User 19 | 20 | func hash(password string) (string, error) { 21 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12) 22 | return string(bytes), err 23 | } 24 | 25 | func checkHash(password string, hash string) bool { 26 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 27 | return err == nil 28 | } 29 | 30 | func generateToken(data common.JSON) (string, error) { 31 | 32 | // token is valid for 7days 33 | date := time.Now().Add(time.Hour * 24 * 7) 34 | 35 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 36 | "user": data, 37 | "exp": date.Unix(), 38 | }) 39 | 40 | // get path from root dir 41 | pwd, _ := os.Getwd() 42 | keyPath := pwd + "/jwtsecret.key" 43 | 44 | key, readErr := ioutil.ReadFile(keyPath) 45 | if readErr != nil { 46 | return "", readErr 47 | } 48 | tokenString, err := token.SignedString(key) 49 | return tokenString, err 50 | } 51 | 52 | func register(c *gin.Context) { 53 | db := c.MustGet("db").(*gorm.DB) 54 | 55 | type RequestBody struct { 56 | Username string `json:"username" binding:"required"` 57 | DisplayName string `json:"display_name" binding:"required"` 58 | Password string `json:"password" binding:"required"` 59 | } 60 | 61 | var body RequestBody 62 | if err := c.BindJSON(&body); err != nil { 63 | c.AbortWithStatus(400) 64 | return 65 | } 66 | 67 | // check existancy 68 | var exists User 69 | if err := db.Where("username = ?", body.Username).First(&exists).Error; err == nil { 70 | c.AbortWithStatus(409) 71 | return 72 | } 73 | 74 | hash, hashErr := hash(body.Password) 75 | if hashErr != nil { 76 | c.AbortWithStatus(500) 77 | return 78 | } 79 | 80 | // create user 81 | user := User{ 82 | Username: body.Username, 83 | DisplayName: body.DisplayName, 84 | PasswordHash: hash, 85 | } 86 | 87 | db.NewRecord(user) 88 | db.Create(&user) 89 | 90 | serialized := user.Serialize() 91 | token, _ := generateToken(serialized) 92 | c.SetCookie("token", token, 60*60*24*7, "/", "", false, true) 93 | 94 | c.JSON(200, common.JSON{ 95 | "user": user.Serialize(), 96 | "token": token, 97 | }) 98 | } 99 | 100 | func login(c *gin.Context) { 101 | db := c.MustGet("db").(*gorm.DB) 102 | type RequestBody struct { 103 | Username string `json:"username" binding:"required"` 104 | Password string `json:"password" binding:"required"` 105 | } 106 | 107 | var body RequestBody 108 | if err := c.BindJSON(&body); err != nil { 109 | c.AbortWithStatus(400) 110 | return 111 | } 112 | 113 | // check existancy 114 | var user User 115 | if err := db.Where("username = ?", body.Username).First(&user).Error; err != nil { 116 | c.AbortWithStatus(404) // user not found 117 | return 118 | } 119 | 120 | if !checkHash(body.Password, user.PasswordHash) { 121 | c.AbortWithStatus(401) 122 | return 123 | } 124 | 125 | serialized := user.Serialize() 126 | token, _ := generateToken(serialized) 127 | 128 | c.SetCookie("token", token, 60*60*24*7, "/", "", false, true) 129 | 130 | c.JSON(200, common.JSON{ 131 | "user": user.Serialize(), 132 | "token": token, 133 | }) 134 | } 135 | 136 | // check API will renew token when token life is less than 3 days, otherwise, return null for token 137 | func check(c *gin.Context) { 138 | userRaw, ok := c.Get("user") 139 | if !ok { 140 | c.AbortWithStatus(401) 141 | return 142 | } 143 | 144 | user := userRaw.(User) 145 | 146 | tokenExpire := int64(c.MustGet("token_expire").(float64)) 147 | now := time.Now().Unix() 148 | diff := tokenExpire - now 149 | 150 | fmt.Println(diff) 151 | if diff < 60*60*24*3 { 152 | // renew token 153 | token, _ := generateToken(user.Serialize()) 154 | c.SetCookie("token", token, 60*60*24*7, "/", "", false, true) 155 | c.JSON(200, common.JSON{ 156 | "token": token, 157 | "user": user.Serialize(), 158 | }) 159 | return 160 | } 161 | 162 | c.JSON(200, common.JSON{ 163 | "token": nil, 164 | "user": user.Serialize(), 165 | }) 166 | } 167 | -------------------------------------------------------------------------------- /api/v1.0/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // ApplyRoutes applies router to the gin Engine 8 | func ApplyRoutes(r *gin.RouterGroup) { 9 | auth := r.Group("/auth") 10 | { 11 | auth.POST("/register", register) 12 | auth.POST("/login", login) 13 | auth.GET("/check", check) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/v1.0/posts/posts.ctrl.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/jinzhu/gorm" 6 | "github.com/velopert/gin-rest-api-sample/database/models" 7 | "github.com/velopert/gin-rest-api-sample/lib/common" 8 | ) 9 | 10 | // Post type alias 11 | type Post = models.Post 12 | 13 | // User type alias 14 | type User = models.User 15 | 16 | // JSON type alias 17 | type JSON = common.JSON 18 | 19 | func create(c *gin.Context) { 20 | db := c.MustGet("db").(*gorm.DB) 21 | type RequestBody struct { 22 | Text string `json:"text" binding:"required"` 23 | } 24 | var requestBody RequestBody 25 | 26 | if err := c.BindJSON(&requestBody); err != nil { 27 | c.AbortWithStatus(400) 28 | return 29 | } 30 | 31 | user := c.MustGet("user").(User) 32 | post := Post{Text: requestBody.Text, User: user} 33 | db.NewRecord(post) 34 | db.Create(&post) 35 | c.JSON(200, post.Serialize()) 36 | } 37 | 38 | func list(c *gin.Context) { 39 | db := c.MustGet("db").(*gorm.DB) 40 | 41 | cursor := c.Query("cursor") 42 | recent := c.Query("recent") 43 | 44 | var posts []Post 45 | 46 | if cursor == "" { 47 | if err := db.Preload("User").Limit(10).Order("id desc").Find(&posts).Error; err != nil { 48 | c.AbortWithStatus(500) 49 | return 50 | } 51 | } else { 52 | condition := "id < ?" 53 | if recent == "1" { 54 | condition = "id > ?" 55 | } 56 | if err := db.Preload("User").Limit(10).Order("id desc").Where(condition, cursor).Find(&posts).Error; err != nil { 57 | c.AbortWithStatus(500) 58 | return 59 | } 60 | } 61 | 62 | length := len(posts) 63 | serialized := make([]JSON, length, length) 64 | 65 | for i := 0; i < length; i++ { 66 | serialized[i] = posts[i].Serialize() 67 | } 68 | 69 | c.JSON(200, serialized) 70 | } 71 | 72 | func read(c *gin.Context) { 73 | db := c.MustGet("db").(*gorm.DB) 74 | id := c.Param("id") 75 | var post Post 76 | 77 | // auto preloads the related model 78 | // http://gorm.io/docs/preload.html#Auto-Preloading 79 | if err := db.Set("gorm:auto_preload", true).Where("id = ?", id).First(&post).Error; err != nil { 80 | c.AbortWithStatus(404) 81 | return 82 | } 83 | 84 | c.JSON(200, post.Serialize()) 85 | } 86 | 87 | func remove(c *gin.Context) { 88 | db := c.MustGet("db").(*gorm.DB) 89 | id := c.Param("id") 90 | 91 | user := c.MustGet("user").(User) 92 | 93 | var post Post 94 | if err := db.Where("id = ?", id).First(&post).Error; err != nil { 95 | c.AbortWithStatus(404) 96 | return 97 | } 98 | 99 | if post.UserID != user.ID { 100 | c.AbortWithStatus(403) 101 | return 102 | } 103 | 104 | db.Delete(&post) 105 | c.Status(204) 106 | } 107 | 108 | func update(c *gin.Context) { 109 | db := c.MustGet("db").(*gorm.DB) 110 | id := c.Param("id") 111 | 112 | user := c.MustGet("user").(User) 113 | 114 | type RequestBody struct { 115 | Text string `json:"text" binding:"required"` 116 | } 117 | 118 | var requestBody RequestBody 119 | 120 | if err := c.BindJSON(&requestBody); err != nil { 121 | c.AbortWithStatus(400) 122 | return 123 | } 124 | 125 | var post Post 126 | if err := db.Preload("User").Where("id = ?", id).First(&post).Error; err != nil { 127 | c.AbortWithStatus(404) 128 | return 129 | } 130 | 131 | if post.UserID != user.ID { 132 | c.AbortWithStatus(403) 133 | return 134 | } 135 | 136 | post.Text = requestBody.Text 137 | db.Save(&post) 138 | c.JSON(200, post.Serialize()) 139 | } 140 | -------------------------------------------------------------------------------- /api/v1.0/posts/posts.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/velopert/gin-rest-api-sample/lib/middlewares" 6 | ) 7 | 8 | // ApplyRoutes applies router to the gin Engine 9 | func ApplyRoutes(r *gin.RouterGroup) { 10 | posts := r.Group("/posts") 11 | { 12 | posts.POST("/", middlewares.Authorized, create) 13 | posts.GET("/", list) 14 | posts.GET("/:id", read) 15 | posts.DELETE("/:id", middlewares.Authorized, remove) 16 | posts.PATCH("/:id", middlewares.Authorized, update) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api/v1.0/v1.0.go: -------------------------------------------------------------------------------- 1 | package apiv1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/velopert/gin-rest-api-sample/api/v1.0/auth" 6 | "github.com/velopert/gin-rest-api-sample/api/v1.0/posts" 7 | ) 8 | 9 | func ping(c *gin.Context) { 10 | c.JSON(200, gin.H{ 11 | "message": "pong", 12 | }) 13 | } 14 | 15 | // ApplyRoutes applies router to the gin Engine 16 | func ApplyRoutes(r *gin.RouterGroup) { 17 | v1 := r.Group("/v1.0") 18 | { 19 | v1.GET("/ping", ping) 20 | auth.ApplyRoutes(v1) 21 | posts.ApplyRoutes(v1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/jinzhu/gorm" 8 | _ "github.com/jinzhu/gorm/dialects/mysql" // configures mysql driver 9 | "github.com/velopert/gin-rest-api-sample/database/models" 10 | ) 11 | 12 | // Initialize initializes the database 13 | func Initialize() (*gorm.DB, error) { 14 | dbConfig := os.Getenv("DB_CONFIG") 15 | db, err := gorm.Open("mysql", dbConfig) 16 | db.LogMode(true) // logs SQL 17 | if err != nil { 18 | panic(err) 19 | } 20 | fmt.Println("Connected to database") 21 | models.Migrate(db) 22 | return db, err 23 | } 24 | -------------------------------------------------------------------------------- /database/inject.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/jinzhu/gorm" 6 | ) 7 | 8 | // Inject injects database to gin context 9 | func Inject(db *gorm.DB) gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | c.Set("db", db) 12 | c.Next() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /database/models/migrate.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | // Migrate automigrates models using ORM 10 | func Migrate(db *gorm.DB) { 11 | db.AutoMigrate(&User{}, &Post{}) 12 | // set up foreign keys 13 | db.Model(&Post{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE") 14 | fmt.Println("Auto Migration has beed processed") 15 | } 16 | -------------------------------------------------------------------------------- /database/models/post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/velopert/gin-rest-api-sample/lib/common" 6 | ) 7 | 8 | // Post data model 9 | type Post struct { 10 | gorm.Model 11 | Text string `sql:"type:text;"` 12 | User User `gorm:"foreignkey:UserID"` 13 | UserID uint 14 | } 15 | 16 | // Serialize serializes post data 17 | func (p Post) Serialize() common.JSON { 18 | return common.JSON{ 19 | "id": p.ID, 20 | "text": p.Text, 21 | "user": p.User.Serialize(), 22 | "created_at": p.CreatedAt, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/velopert/gin-rest-api-sample/lib/common" 6 | ) 7 | 8 | // User data model 9 | type User struct { 10 | gorm.Model 11 | Username string 12 | DisplayName string 13 | PasswordHash string 14 | } 15 | 16 | // Serialize serializes user data 17 | func (u *User) Serialize() common.JSON { 18 | return common.JSON{ 19 | "id": u.ID, 20 | "username": u.Username, 21 | "display_name": u.DisplayName, 22 | } 23 | } 24 | 25 | func (u *User) Read(m common.JSON) { 26 | u.ID = uint(m["id"].(float64)) 27 | u.Username = m["username"].(string) 28 | u.DisplayName = m["display_name"].(string) 29 | } 30 | -------------------------------------------------------------------------------- /jwtsecret.key: -------------------------------------------------------------------------------- 1 | m/ATjN6q8rEjMyXaX/QIoKrhyn9Cw3wtkSwQPrTe4KosTVX3wTRyo7r8Bz03dneK 2 | B5OeJVIOHzeorY3ykPINOc/3hNiRQzgffqQA1dzGzhbZK+waZrIEbpFu5t8xRIzM 3 | ek9mDvzK7K8NGA2m/QDYQa7v1R2yXcCBJgwUv5koTvxhRGbcFjYEMJG8/NIt5zfg 4 | oyvEgyD2oqPpD1n4iJLVrgkQjnNPasABVvX3Gylj13LDnqWkqbWMnkQE4qz0fbMi 5 | qnebAGPgYLDbEd7M/XCHXsSQXGI0InZeNZECUqfA0Oc4eq1GpQXwFcIF9QsDJveg 6 | lv+C3Vv+mTSAcrv/tmGsbg== 7 | -------------------------------------------------------------------------------- /lib/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // JSON alias type 4 | type JSON = map[string]interface{} 5 | -------------------------------------------------------------------------------- /lib/middlewares/authorized.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // Authorized blocks unauthorized requestrs 6 | func Authorized(c *gin.Context) { 7 | _, exists := c.Get("user") 8 | if !exists { 9 | c.AbortWithStatus(401) 10 | return 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/middlewares/jwt.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | jwt "github.com/dgrijalva/jwt-go" 11 | "github.com/gin-gonic/gin" 12 | "github.com/velopert/gin-rest-api-sample/database/models" 13 | "github.com/velopert/gin-rest-api-sample/lib/common" 14 | ) 15 | 16 | var secretKey []byte 17 | 18 | func init() { 19 | // get path from root dir 20 | pwd, _ := os.Getwd() 21 | keyPath := pwd + "/jwtsecret.key" 22 | 23 | key, readErr := ioutil.ReadFile(keyPath) 24 | if readErr != nil { 25 | panic("failed to load secret key file") 26 | } 27 | secretKey = key 28 | } 29 | 30 | func validateToken(tokenString string) (common.JSON, error) { 31 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 32 | // Don't forget to validate the alg is what you expect: 33 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 34 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 35 | } 36 | 37 | return secretKey, nil 38 | }) 39 | 40 | if err != nil { 41 | return common.JSON{}, err 42 | } 43 | 44 | if !token.Valid { 45 | return common.JSON{}, errors.New("invalid token") 46 | } 47 | 48 | return token.Claims.(jwt.MapClaims), nil 49 | } 50 | 51 | // JWTMiddleware parses JWT token from cookie and stores data and expires date to the context 52 | // JWT Token can be passed as cookie, or Authorization header 53 | func JWTMiddleware() gin.HandlerFunc { 54 | return func(c *gin.Context) { 55 | tokenString, err := c.Cookie("token") 56 | // failed to read cookie 57 | if err != nil { 58 | // try reading HTTP Header 59 | authorization := c.Request.Header.Get("Authorization") 60 | if authorization == "" { 61 | c.Next() 62 | return 63 | } 64 | sp := strings.Split(authorization, "Bearer ") 65 | // invalid token 66 | if len(sp) < 1 { 67 | c.Next() 68 | return 69 | } 70 | tokenString = sp[1] 71 | } 72 | 73 | tokenData, err := validateToken(tokenString) 74 | if err != nil { 75 | c.Next() 76 | return 77 | } 78 | 79 | var user models.User 80 | user.Read(tokenData["user"].(common.JSON)) 81 | 82 | c.Set("user", user) 83 | c.Set("token_expire", tokenData["exp"]) 84 | c.Next() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/joho/godotenv" 8 | "github.com/velopert/gin-rest-api-sample/api" 9 | "github.com/velopert/gin-rest-api-sample/database" 10 | "github.com/velopert/gin-rest-api-sample/lib/middlewares" 11 | ) 12 | 13 | func main() { 14 | // load .env environment variables 15 | err := godotenv.Load() 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | // initializes database 21 | db, _ := database.Initialize() 22 | 23 | port := os.Getenv("PORT") 24 | app := gin.Default() // create gin app 25 | app.Use(database.Inject(db)) 26 | app.Use(middlewares.JWTMiddleware()) 27 | api.ApplyRoutes(app) // apply api router 28 | app.Run(":" + port) // listen to given port 29 | } 30 | -------------------------------------------------------------------------------- /scripts/start-dev: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | gin -p 4001 -a 4000 run main.go --------------------------------------------------------------------------------