diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..0c5e861 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +GinTutorial \ No newline at end of file diff --git a/.idea/GinWithGormTutorial.iml b/.idea/GinTutorial.iml similarity index 100% rename from .idea/GinWithGormTutorial.iml rename to .idea/GinTutorial.iml diff --git a/.idea/modules.xml b/.idea/modules.xml index 1bfce4f..6ffb793 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/config/config.go b/config/config.go index a131b41..3a924a5 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,9 @@ type Config struct { Name string Charset string } + Bcrypt struct { + Cost int + } } var AppConfig *Config diff --git a/config/db.go b/config/db.go index 34e34d1..e4b1a54 100644 --- a/config/db.go +++ b/config/db.go @@ -1,7 +1,7 @@ package config import ( - "GinWithGormTutorial/global" + "GinTutorial/global" "fmt" "log" "time" @@ -11,7 +11,7 @@ import ( ) func buildDSN(user, password, host, port, db, charset string) string { - template := "%s:%s@tcp(%s:%s)/%s?charset=%s" + template := "%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=true" return fmt.Sprintf(template, user, password, host, port, db, charset) } diff --git a/controllers/auth_controller.go b/controllers/auth_controller.go new file mode 100644 index 0000000..a353382 --- /dev/null +++ b/controllers/auth_controller.go @@ -0,0 +1,102 @@ +package controllers + +import ( + "GinTutorial/global" + "GinTutorial/models" + "GinTutorial/utils" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +func Register(ctx *gin.Context) { + var user models.User + + if err := ctx.ShouldBindJSON(&user); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + return + } + + hashPassword, err := utils.HashPassword(user.Password) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + } + + user.Password = hashPassword + + if err := global.Db.AutoMigrate(&user); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + if err := global.Db.Create(&user).Error; err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + token, err := utils.GenerateJWT(strconv.Itoa(int(user.ID))) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "token": token, + }) +} + +func Login(ctx *gin.Context) { + var InputUser struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + } + + if err := ctx.ShouldBindJSON(&InputUser); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + return + } + + var user models.User + + if err := global.Db.Where("username = ?", InputUser.Username).First(&user).Error; err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "error": "Invalid username or password", + }) + return + } + + if !utils.CheckPassword(InputUser.Password, user.Password) { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "error": "Invalid username or password", + }) + return + } + + token, err := utils.GenerateJWT(strconv.Itoa(int(user.ID))) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "token": token, + }) +} diff --git a/go.mod b/go.mod index efe6aa0..f209933 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ -module GinWithGormTutorial +module GinTutorial go 1.24 require ( github.com/gin-gonic/gin v1.10.1 + github.com/golang-jwt/jwt/v5 v5.2.3 github.com/spf13/viper v1.20.1 + golang.org/x/crypto v0.40.0 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.30.0 ) @@ -45,8 +47,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.33.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect google.golang.org/protobuf v1.36.1 // indirect diff --git a/go.sum b/go.sum index 2dbdda2..504c590 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -102,10 +104,10 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= diff --git a/main.go b/main.go index dd90045..0964eff 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( - "GinWithGormTutorial/config" - "GinWithGormTutorial/router" + "GinTutorial/config" + "GinTutorial/router" ) func main() { diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..1880a0b --- /dev/null +++ b/models/user.go @@ -0,0 +1,11 @@ +package models + +import ( + "gorm.io/gorm" +) + +type User struct { + gorm.Model + Username string `grom:"unique"` + Password string `grom:"size:255" gorm:"not null"` +} diff --git a/router/router.go b/router/router.go index 8d3692e..15e1417 100644 --- a/router/router.go +++ b/router/router.go @@ -1,6 +1,10 @@ package router -import "github.com/gin-gonic/gin" +import ( + "GinTutorial/controllers" + + "github.com/gin-gonic/gin" +) func SetupRouter() *gin.Engine { r := gin.Default() @@ -10,12 +14,8 @@ func SetupRouter() *gin.Engine { { auth := v1.Group("/auth") { - auth.POST("/login", func(context *gin.Context) { - context.JSON(200, gin.H{ - "msg": "Login successfully", - }) - }) - auth.POST("/register") + auth.POST("/login", controllers.Login) + auth.POST("/register", controllers.Register) } } } diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..cc118fe --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,29 @@ +package utils + +import ( + "GinTutorial/config" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), config.AppConfig.Bcrypt.Cost) + return string(hash), err +} + +func GenerateJWT(username string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{ + "username": username, + "exp": time.Now().Add(time.Hour * 72).Unix(), + }) + + signedToken, err := token.SignedString([]byte("secret")) + return "Bearer " + signedToken, err +} + +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +}