diff --git a/.env b/.env index 9952f95..8a36010 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ PORT=8000 +SECRET=sdc87sdf6gv87df6v8sd7f6v8sdf76v8sdf7v6 DB_URL="host=dumbo.db.elephantsql.com user=yvvpeiee password=QwAhcTtiKL8j9qRyUBIqs9kNbu3O0_FB dbname=yvvpeiee port=5432 sslmode=disable" \ No newline at end of file diff --git a/controllers/usersController.go b/controllers/usersController.go new file mode 100644 index 0000000..0a734d8 --- /dev/null +++ b/controllers/usersController.go @@ -0,0 +1,116 @@ +package controllers + +import ( + "net/http" + "os" + "time" + + "go-crud/initializers" + "go-crud/models" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" + "golang.org/x/crypto/bcrypt" +) + +func SignUp(c *gin.Context) { + // Get the email/pass off req body + var body struct { + Email string + Password string + } + if err := c.ShouldBind(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Failed to read body", + }) + return + } + + // Hash the password + hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Failed to generate pass hash", + }) + return + } + + // Create the user + user := models.User{Email: body.Email, Password: string(hash)} + result := initializers.DB.Create(&user) + + if result.Error != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Failed to create user", + }) + return + } + + // Respond + c.JSON(http.StatusOK, gin.H{}) +} + +func LogIn(c *gin.Context) { + // Get the email/pass off req body + var body struct { + Email string + Password string + } + if err := c.ShouldBind(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Failed to read body", + }) + return + } + + // Look up requested user + var user models.User + initializers.DB.First(&user, "email = ?", body.Email) + if user.ID == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid email or pass", + }) + return + } + + // Compare sent in pass with saved user pass hash + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password)) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid email or pass", + }) + return + } + + // Generate a jwt token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": user.ID, + "exp": time.Now().Add(time.Hour * 24 * 30).Unix(), + }) + + // Sign and get the complete encoded token as a string using the secret token + tokenString, err := token.SignedString([]byte(os.Getenv("SECRET"))) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Failed to create token", + }) + return + } + + // Send it back + c.SetSameSite(http.SameSiteLaxMode) + c.SetCookie("Authorization", tokenString, 3600*24*30, "", "", false, true) + c.JSON(http.StatusOK, gin.H{ + "token": tokenString, + }) +} + +func Validate(c *gin.Context) { + user, _ := c.Get("user") + + c.JSON(http.StatusOK, gin.H{ + "message": user, + }) +} diff --git a/go-crud.exe b/go-crud.exe index 9067b11..c9eddab 100644 Binary files a/go-crud.exe and b/go-crud.exe differ diff --git a/go.mod b/go.mod index e1429c9..e21db02 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require gorm.io/driver/postgres v1.5.2 require ( github.com/bytedance/sonic v1.9.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/fatih/color v1.9.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect @@ -17,6 +18,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.1 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.3.1 // indirect diff --git a/go.sum b/go.sum index e32630f..982b761 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhD github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= @@ -26,6 +28,8 @@ github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+j github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 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/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/main.go b/main.go index 2f0dd0a..11f9c14 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "go-crud/controllers" "go-crud/initializers" + "go-crud/middleware" "github.com/gin-gonic/gin" ) @@ -14,6 +15,10 @@ func init() { func main() { r := gin.Default() + r.POST("/signup", controllers.SignUp) + r.POST("/login", controllers.LogIn) + r.GET("/validate", middleware.RequireAuth, controllers.Validate) + r.POST("/posts", controllers.PostsCreate) r.PUT("/posts/:id", controllers.PostsUpdate) r.GET("/posts", controllers.PostsIndex) diff --git a/middleware/requireAuth.go b/middleware/requireAuth.go new file mode 100644 index 0000000..cb74e64 --- /dev/null +++ b/middleware/requireAuth.go @@ -0,0 +1,65 @@ +package middleware + +import ( + "fmt" + "net/http" + "os" + "time" + + "go-crud/initializers" + "go-crud/models" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" +) + +func RequireAuth(c *gin.Context) { + // Get the cookie off req + tokenString, err := c.Cookie("Authorization") + if err != nil { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // Decode/validate it + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") + return []byte(os.Getenv("SECRET")), nil + }) + + if err != nil || !token.Valid { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // Check the exp + if exp, ok := claims["exp"].(float64); ok && float64(time.Now().Unix()) > exp { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // Find the user with token sub + var user models.User + initializers.DB.First(&user, claims["sub"]) + + if user.ID == 0 { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // Attach to req + c.Set("user", user) + + // Continue + c.Next() +} diff --git a/migrate/migrate.go b/migrate/migrate.go index b3b4bac..72cb7ab 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -11,4 +11,5 @@ func init() { func main() { initializers.DB.AutoMigrate(&models.Post{}) initializers.DB.AutoMigrate(&models.Event{}) + initializers.DB.AutoMigrate(&models.User{}) } \ No newline at end of file diff --git a/models/userModel.go b/models/userModel.go new file mode 100644 index 0000000..b4260da --- /dev/null +++ b/models/userModel.go @@ -0,0 +1,9 @@ +package models + +import "gorm.io/gorm" + +type User struct { + gorm.Model + Email string `gorm:"unique"` + Password string +}