-
Notifications
You must be signed in to change notification settings - Fork 50
4. Using MySQL
myapi
│--- main.go
│--- go.mod
│--- go.sum
│--- .env
│
└─── controller
│ └--- auth.go
│ └--- login.go
│ └--- user.go
│ └--- post.go
│
└─── migrate
│ └--- autoMigrate.go
│
└─── model
│ └--- auth.go
│ └--- user.go
│ └--- post.go
- Create a project directory
myapi
- Create sub-directories
controller
,migrate
andmodel
insidemyapi
- Modify
.env.sample
file according to your database credentials and save it as.env
:
APP_PORT=1200
APP_ENV=development
ACCESS_KEY=Use_a_strong_and_long_random_key
REFRESH_KEY=Use_another_strong_and_long_random_key
# expires tokens in minutes
ACCESS_KEY_TTL=5
REFRESH_KEY_TTL=60
AUDIENCE=
ISSUER=gorest
# NotBefore for ACCESS_KEY in seconds
NOT_BEFORE_ACC=0
# NotBefore for REFRESH_KEY in seconds
NOT_BEFORE_REF=0
SUBJECT=
ACTIVATE_RDBMS=yes
DBDRIVER=mysql
DBUSER=dbUser
DBPASS=dbPass
DBNAME=dbName
DBHOST=localhost
DBPORT=3306
DBSSLMODE=disable
DBTIMEZONE=your_server's_timezone
Illegal symbol: Do not use #
in any environment variable.
In this tutorial, the following schema diagram is used for database design.
For auths
, users
, and posts
tables, create the respective models as structs.
myapi
└─── model
│ └--- auth.go
│ └--- user.go
│ └--- post.go
package model
import (
"time"
"gorm.io/gorm"
)
// Auth model - `auths` table
type Auth struct {
AuthID uint64 `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Email string `json:"Email"`
Password string `json:"Password"`
User User `gorm:"foreignkey:IDAuth;references:AuthID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
package model
import (
"time"
"gorm.io/gorm"
)
// User model - `users` table
type User struct {
UserID uint64 `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
FirstName string `json:"FirstName,omitempty"`
LastName string `json:"LastName,omitempty"`
IDAuth uint64 `json:"-"`
Posts []Post `gorm:"foreignkey:IDUser;references:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:",omitempty"`
}
package model
import (
"time"
"gorm.io/gorm"
)
// Post model - `posts` table
type Post struct {
PostID uint64 `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Title string `json:"Title,omitempty"`
Body string `json:"Body,omitempty"`
IDUser uint64 `json:"-"`
}
Collected from: https://github.com/pilinux/gorest/blob/main/database/migrate/autoMigrate.go
myapi
└─── migrate
│ └--- autoMigrate.go
// autoMigrate.go needs to be executed only when it is required
package main
import (
"fmt"
"gorm.io/gorm"
"github.com/pilinux/gorest/config"
"github.com/pilinux/gorest/database"
"myapi/model"
)
// Load all the models
type auth model.Auth
type user model.User
type post model.Post
var db *gorm.DB
var errorState int
func main() {
configureDB := config.Database().RDBMS
driver := configureDB.Env.Driver
/*
** 0 = default/no error
** 1 = error
**/
errorState = 0
db = database.InitDB()
// Auto migration
/*
- Automatically migrate schema
- Only create tables with missing columns and missing indexes
- Will not change/delete any existing columns and their types
*/
// Careful! It will drop all the tables!
dropAllTables()
// Automatically migrate all the tables
migrateTables()
// Manually set foreign keys for MySQL and PostgreSQL
if driver != "sqlite3" {
setPkFk()
}
if errorState == 0 {
fmt.Println("Auto migration is completed!")
} else {
fmt.Println("Auto migration failed!")
}
}
func dropAllTables() {
// Careful! It will drop all the tables!
if err := db.Migrator().DropTable(&post{}, &user{}, &auth{}); err != nil {
errorState = 1
fmt.Println(err)
} else {
fmt.Println("Old tables are deleted!")
}
}
func migrateTables() {
configureDB := config.Database().RDBMS
driver := configureDB.Env.Driver
if driver == "mysql" {
// db.Set() --> add table suffix during auto migration
if err := db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&auth{},
&user{}, &post{}); err != nil {
errorState = 1
fmt.Println(err)
} else {
fmt.Println("New tables are migrated successfully!")
}
} else {
if err := db.AutoMigrate(&auth{},
&user{}, &post{}); err != nil {
errorState = 1
fmt.Println(err)
} else {
fmt.Println("New tables are migrated successfully!")
}
}
}
func setPkFk() {
// Manually set foreign key for MySQL and PostgreSQL
if err := db.Migrator().CreateConstraint(&auth{}, "User"); err != nil {
errorState = 1
fmt.Println(err)
}
if err := db.Migrator().CreateConstraint(&user{}, "Posts"); err != nil {
errorState = 1
fmt.Println(err)
}
}
Note: The following packages are imported from gorest
for database configuration.
"github.com/pilinux/gorest/config"
"github.com/pilinux/gorest/database"
At the root level (myapi
):
go mod init myapi
go mod tidy -compat=1.17
Now, inside migrate
directory, build the executable file:
cd migrate
go build
On the database server where MySQL is installed and the DB is configured,
save the .env
file and execute this newly built migrate
file
to migrate the database.
myapi
└─── controller
│ └--- auth.go
│ └--- login.go
│ └--- user.go
│ └--- post.go
Concept:
- A new user creates a new account with an email and password
- The user logs into the system
- After authentication, user receives an access token and a refresh token
- With the access token, user creates his profile (First Name & Last Name)
- With the access token, now the user can create blogs (Title & Body)
package model
import (
"encoding/json"
"errors"
"time"
"gorm.io/gorm"
"github.com/pilinux/gorest/config"
"github.com/pilinux/gorest/lib"
)
// Auth model - `auths` table
type Auth struct {
AuthID uint64 `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Email string `json:"Email"`
Password string `json:"Password"`
User User `gorm:"foreignkey:IDAuth;references:AuthID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
// UnmarshalJSON ...
func (v *Auth) UnmarshalJSON(b []byte) error {
aux := struct {
AuthID uint64 `json:"AuthID"`
Email string `json:"Email"`
Password string `json:"Password"`
}{}
if err := json.Unmarshal(b, &aux); err != nil {
return err
}
// check password length
// if more checks are required i.e. password pattern,
// add all conditions here
if len(aux.Password) < 6 {
return errors.New("short password")
}
v.AuthID = aux.AuthID
v.Email = aux.Email
config := lib.HashPassConfig{
Memory: config.Security().HashPass.Memory,
Iterations: config.Security().HashPass.Iterations,
Parallelism: config.Security().HashPass.Parallelism,
SaltLength: config.Security().HashPass.SaltLength,
KeyLength: config.Security().HashPass.KeyLength,
}
pass, err := lib.HashPass(config, aux.Password)
if err != nil {
return err
}
v.Password = pass
return nil
}
// MarshalJSON ...
func (v Auth) MarshalJSON() ([]byte, error) {
aux := struct {
AuthID uint64 `json:"AuthId"`
Email string `json:"Email"`
}{
AuthID: v.AuthID,
Email: v.Email,
}
return json.Marshal(aux)
}
Note: github.com/pilinux/gorest/database/model
is imported as pmodel
to access HashPass
function.
package model
import (
"encoding/json"
"errors"
"time"
pmodel "github.com/pilinux/gorest/database/model"
"gorm.io/gorm"
)
// Auth model - `auths` table
type Auth struct {
AuthID uint64 `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Email string `json:"Email"`
Password string `json:"Password"`
User User `gorm:"foreignkey:IDAuth;references:AuthID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
// UnmarshalJSON ...
func (v *Auth) UnmarshalJSON(b []byte) error {
aux := struct {
AuthID uint64 `json:"AuthID"`
Email string `json:"Email"`
Password string `json:"Password"`
}{}
if err := json.Unmarshal(b, &aux); err != nil {
return err
}
// check password length
// if more checks are required i.e. password pattern,
// add all conditions here
if len(aux.Password) < 6 {
return errors.New("short password")
}
v.AuthID = aux.AuthID
v.Email = aux.Email
if v.Password = pmodel.HashPass(aux.Password); v.Password == "error" {
return errors.New("HashPass failed")
}
return nil
}
// MarshalJSON ...
func (v Auth) MarshalJSON() ([]byte, error) {
aux := struct {
AuthID uint64 `json:"AuthId"`
Email string `json:"Email"`
}{
AuthID: v.AuthID,
Email: v.Email,
}
return json.Marshal(aux)
}
Collected from: https://github.com/pilinux/gorest/blob/main/controller/auth.go
- [HTTP POST] http://localhost:1200/api/v1/register
Create a new user account
{
"Email":"[email protected]",
"Password":"... ..."
}
Note:
-
github.com/pilinux/gorest/database
: connect to the MySQL database -
github.com/pilinux/gorest/lib
:ValidateEmail
- check if the email format is valid (v1.5.x
) -
github.com/pilinux/gorest/service
:IsEmailValid
- check if the email format is valid (<=v1.4.5
)
package controller
import (
"net/http"
"github.com/pilinux/gorest/database"
"github.com/pilinux/gorest/lib"
"github.com/pilinux/gorest/lib/renderer"
"github.com/gin-gonic/gin"
"myapi/model"
)
// CreateUserAuth - POST /register
func CreateUserAuth(c *gin.Context) {
db := database.GetDB()
auth := model.Auth{}
authFinal := model.Auth{}
// bind JSON
if err := c.ShouldBindJSON(&auth); err != nil {
renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
return
}
// email validation
if !lib.ValidateEmail(auth.Email) {
renderer.Render(c, gin.H{"msg": "wrong email address"}, http.StatusBadRequest)
return
}
// email must be unique
if err := db.Where("email = ?", auth.Email).First(&auth).Error; err == nil {
renderer.Render(c, gin.H{"msg": "email already registered"}, http.StatusForbidden)
return
}
// user must not be able to manipulate all fields
authFinal.Email = auth.Email
authFinal.Password = auth.Password
// one unique email for each account
tx := db.Begin()
if err := tx.Create(&authFinal).Error; err != nil {
tx.Rollback()
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
} else {
tx.Commit()
renderer.Render(c, authFinal, http.StatusCreated)
}
}
Collected from: https://github.com/pilinux/gorest/blob/main/controller/login.go
Tasks:
- [HTTP POST] http://localhost:1200/api/v1/login
Verify email-password and issue new access and refresh tokens
{
"Email":"[email protected]",
"Password":"..."
}
- [HTTP POST] http://localhost:1200/api/v1/refresh
Verify existing refresh token and issue new access and refresh tokens
{
"RefreshJWT":"use_existing_valid_refresh_token"
}
Note: github.com/pilinux/gorest/lib/middleware
to handle JWT authorization
package controller
import (
"net/http"
"github.com/pilinux/gorest/lib"
"github.com/pilinux/gorest/lib/middleware"
"github.com/pilinux/gorest/lib/renderer"
"github.com/pilinux/gorest/service"
"github.com/alexedwards/argon2id"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
// LoginPayload ...
type LoginPayload struct {
Email string `json:"Email"`
Password string `json:"Password"`
}
// Login ...
func Login(c *gin.Context) {
var payload LoginPayload
if err := c.ShouldBindJSON(&payload); err != nil {
renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
return
}
if !lib.ValidateEmail(payload.Email) {
renderer.Render(c, gin.H{"msg": "wrong email address"}, http.StatusBadRequest)
return
}
v, err := service.GetUserByEmail(payload.Email)
if err != nil {
renderer.Render(c, gin.H{"msg": "not found"}, http.StatusNotFound)
return
}
verifyPass, err := argon2id.ComparePasswordAndHash(payload.Password, v.Password)
if err != nil {
log.WithError(err).Error("error code: 1011")
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
return
}
if !verifyPass {
renderer.Render(c, gin.H{"msg": "wrong credentials"}, http.StatusUnauthorized)
return
}
accessJWT, _, err := middleware.GetJWT(v.AuthID, v.Email, "", "", "", "", "", "access")
if err != nil {
log.WithError(err).Error("error code: 1012")
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
return
}
refreshJWT, _, err := middleware.GetJWT(v.AuthID, v.Email, "", "", "", "", "", "refresh")
if err != nil {
log.WithError(err).Error("error code: 1013")
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
return
}
jwtPayload := middleware.JWTPayload{}
jwtPayload.AccessJWT = accessJWT
jwtPayload.RefreshJWT = refreshJWT
renderer.Render(c, jwtPayload, http.StatusOK)
}
// Refresh ...
func Refresh(c *gin.Context) {
authID := middleware.AuthID
email := middleware.Email
// check validity
if authID == 0 {
renderer.Render(c, gin.H{"msg": "access denied"}, http.StatusUnauthorized)
return
}
if email == "" {
renderer.Render(c, gin.H{"msg": "access denied"}, http.StatusUnauthorized)
return
}
// issue new tokens
accessJWT, _, err := middleware.GetJWT(authID, email, "", "", "", "", "", "access")
if err != nil {
log.WithError(err).Error("error code: 1021")
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
return
}
refreshJWT, _, err := middleware.GetJWT(authID, email, "", "", "", "", "", "refresh")
if err != nil {
log.WithError(err).Error("error code: 1022")
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
return
}
jwtPayload := middleware.JWTPayload{}
jwtPayload.AccessJWT = accessJWT
jwtPayload.RefreshJWT = refreshJWT
renderer.Render(c, jwtPayload, http.StatusOK)
}
Collected from: https://github.com/pilinux/gorest/blob/main/controller/user.go
Tasks:
- [HTTP GET] http://localhost:1200/api/v1/users/:id
Fetch user details (first & last name) along with all blog posts belonged to the user
- [HTTP POST] http://localhost:1200/api/v1/users
Create a new user profile [requires JWT for verification]
{
"FirstName": "...",
"LastName": "..."
}
- [HTTP PUT] http://localhost:1200/api/v1/users
Update personal user profile [requires JWT for verification]
{
"FirstName": "...",
"LastName": "..."
}
Note:
-
github.com/pilinux/gorest/database
: connect to the MySQL database -
github.com/pilinux/gorest/lib/middleware
: to handle JWT authorization
package controller
import (
"net/http"
"time"
"github.com/pilinux/gorest/database"
"github.com/pilinux/gorest/lib/middleware"
"github.com/pilinux/gorest/lib/renderer"
"github.com/gin-gonic/gin"
"myapi/model"
)
// GetUser - GET /users/:id
func GetUser(c *gin.Context) {
db := database.GetDB()
id := c.Params.ByName("id")
user := model.User{}
posts := []model.Post{}
if err := db.Where("user_id = ? ", id).First(&user).Error; err != nil {
renderer.Render(c, gin.H{"msg": "not found"}, http.StatusNotFound)
} else {
db.Model(&posts).Where("id_user = ?", id).Find(&posts)
user.Posts = posts
renderer.Render(c, user, http.StatusOK)
}
}
// CreateUser - POST /users
func CreateUser(c *gin.Context) {
db := database.GetDB()
user := model.User{}
userFinal := model.User{}
userIDAuth := middleware.AuthID
// does the user have an existing profile
if err := db.Where("id_auth = ?", userIDAuth).First(&userFinal).Error; err == nil {
renderer.Render(c, gin.H{"msg": "user profile found, no need to create a new one"}, http.StatusForbidden)
return
}
// bind JSON
if err := c.ShouldBindJSON(&user); err != nil {
renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
return
}
// user must not be able to manipulate all fields
userFinal.FirstName = user.FirstName
userFinal.LastName = user.LastName
userFinal.IDAuth = userIDAuth
tx := db.Begin()
if err := tx.Create(&userFinal).Error; err != nil {
tx.Rollback()
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
} else {
tx.Commit()
renderer.Render(c, userFinal, http.StatusCreated)
}
}
// UpdateUser - PUT /users
func UpdateUser(c *gin.Context) {
db := database.GetDB()
user := model.User{}
userFinal := model.User{}
userIDAuth := middleware.AuthID
// does the user have an existing profile
if err := db.Where("id_auth = ?", userIDAuth).First(&userFinal).Error; err != nil {
renderer.Render(c, gin.H{"msg": "no user profile found"}, http.StatusNotFound)
return
}
// bind JSON
if err := c.ShouldBindJSON(&user); err != nil {
renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
return
}
// user must not be able to manipulate all fields
userFinal.UpdatedAt = time.Now()
userFinal.FirstName = user.FirstName
userFinal.LastName = user.LastName
tx := db.Begin()
if err := tx.Save(&userFinal).Error; err != nil {
tx.Rollback()
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
} else {
tx.Commit()
renderer.Render(c, userFinal, http.StatusOK)
}
}
Collected from: https://github.com/pilinux/gorest/blob/main/controller/post.go
Tasks:
- [HTTP GET] http://localhost:1200/api/v1/posts
Fetch all blog posts saved in the database
- [HTTP GET] http://localhost:1200/api/v1/posts/:id
Fetch a post by its ID
- [HTTP POST] http://localhost:1200/api/v1/posts
Post a new blog article [requires JWT for verification]
{
"Title": "...",
"Body": "... ..."
}
Note:
-
github.com/pilinux/gorest/database
: connect to the MySQL database -
github.com/pilinux/gorest/lib/middleware
: to handle JWT authorization
package controller
import (
"net/http"
"github.com/pilinux/gorest/database"
"github.com/pilinux/gorest/lib/middleware"
"github.com/pilinux/gorest/lib/renderer"
"github.com/gin-gonic/gin"
"myapi/model"
)
// GetPosts - GET /posts
func GetPosts(c *gin.Context) {
db := database.GetDB()
posts := []model.Post{}
if err := db.Find(&posts).Error; err != nil {
renderer.Render(c, gin.H{"msg": "not found"}, http.StatusNotFound)
} else {
renderer.Render(c, posts, http.StatusOK)
}
}
// GetPost - GET /posts/:id
func GetPost(c *gin.Context) {
db := database.GetDB()
post := model.Post{}
id := c.Params.ByName("id")
if err := db.Where("post_id = ? ", id).First(&post).Error; err != nil {
renderer.Render(c, gin.H{"msg": "not found"}, http.StatusNotFound)
} else {
renderer.Render(c, post, http.StatusOK)
}
}
// CreatePost - POST /posts
func CreatePost(c *gin.Context) {
db := database.GetDB()
user := model.User{}
post := model.Post{}
postFinal := model.Post{}
userIDAuth := middleware.AuthID
// does the user have an existing profile
if err := db.Where("id_auth = ?", userIDAuth).First(&user).Error; err != nil {
renderer.Render(c, gin.H{"msg": "no user profile found"}, http.StatusForbidden)
return
}
// bind JSON
if err := c.ShouldBindJSON(&post); err != nil {
renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
return
}
// user must not be able to manipulate all fields
postFinal.Title = post.Title
postFinal.Body = post.Body
postFinal.IDUser = user.UserID
tx := db.Begin()
if err := tx.Create(&postFinal).Error; err != nil {
tx.Rollback()
renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
} else {
tx.Commit()
renderer.Render(c, postFinal, http.StatusCreated)
}
}
main.go:
Collected from: https://github.com/pilinux/gorest/blob/main/main.go
package main
import (
"fmt"
"myapi/controller"
"github.com/gin-gonic/gin"
"github.com/pilinux/gorest/config"
"github.com/pilinux/gorest/database"
"github.com/pilinux/gorest/lib/middleware"
)
var configure = config.Config()
func main() {
if configure.Database.RDBMS.Activate == "yes" {
// Initialize RDBMS client
if err := database.InitDB().Error; err != nil {
fmt.Println(err)
return
}
}
// JWT
middleware.AccessKey = []byte(configure.Security.JWT.AccessKey)
middleware.AccessKeyTTL = configure.Security.JWT.AccessKeyTTL
middleware.RefreshKey = []byte(configure.Security.JWT.RefreshKey)
middleware.RefreshKeyTTL = configure.Security.JWT.RefreshKeyTTL
router, err := SetupRouter()
if err != nil {
fmt.Println(err)
return
}
err = router.Run(":" + configure.Server.ServerPort)
if err != nil {
fmt.Println(err)
return
}
}
// SetupRouter ...
func SetupRouter() (*gin.Engine, error) {
if configure.Server.ServerEnv == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.Default()
router.Use(middleware.CORS(
configure.Security.CORS.Origin,
configure.Security.CORS.Credentials,
configure.Security.CORS.Headers,
configure.Security.CORS.Methods,
configure.Security.CORS.MaxAge,
))
// For gorest <= v1.4.5
// router.Use(middleware.CORS())
// API:v1
v1 := router.Group("/api/v1/")
{
// RDBMS
if configure.Database.RDBMS.Activate == "yes" {
// Register - no JWT required
v1.POST("register", controller.CreateUserAuth)
// Login - app issues JWT
v1.POST("login", controller.Login)
// Refresh - app issues new JWT
rJWT := v1.Group("refresh")
rJWT.Use(middleware.RefreshJWT())
rJWT.POST("", controller.Refresh)
// User
rUsers := v1.Group("users")
rUsers.GET("/:id", controller.GetUser) // Non-protected
rUsers.Use(middleware.JWT())
rUsers.POST("", controller.CreateUser) // Protected
rUsers.PUT("", controller.UpdateUser)
// Post
rPosts := v1.Group("posts")
rPosts.GET("", controller.GetPosts) // Non-protected
rPosts.GET("/:id", controller.GetPost) // Non-protected
rPosts.Use(middleware.JWT())
rPosts.POST("", controller.CreatePost) // Protected
}
}
return router, nil
}
go mod tidy
go build
./myapi