feat: add user register and login routes
parent
eb862be55d
commit
fce349a31d
|
|
@ -0,0 +1,19 @@
|
|||
### Register a new user
|
||||
POST http://localhost:8080/auth/register/
|
||||
content-type: application/json
|
||||
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"name" : "Test User",
|
||||
"password": "examplePassword",
|
||||
"password_confirm": "examplePassword"
|
||||
}
|
||||
|
||||
### Login
|
||||
POST http://localhost:8080/auth/login/
|
||||
content-type: application/json
|
||||
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"password": "examplePassword"
|
||||
}
|
||||
6
go.mod
6
go.mod
|
|
@ -18,6 +18,8 @@ require (
|
|||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.22.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
|
|
@ -28,9 +30,9 @@ require (
|
|||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -25,9 +25,13 @@ github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4
|
|||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
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=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
|
|
@ -70,12 +74,16 @@ 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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
|
|
|
|||
9
main.go
9
main.go
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
"net/http"
|
||||
"udemy_httpserver/db"
|
||||
"udemy_httpserver/routes"
|
||||
|
|
@ -9,12 +10,18 @@ import (
|
|||
|
||||
func main() {
|
||||
db.InitDB(db.Conn)
|
||||
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
panic("Error loading .env file")
|
||||
}
|
||||
|
||||
server := gin.Default()
|
||||
|
||||
server.GET("/", henloWolrd)
|
||||
routes.RegisterRoutes(server)
|
||||
|
||||
err := server.Run(":8080")
|
||||
err = server.Run(":8080")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"os"
|
||||
"time"
|
||||
"udemy_httpserver/db"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string `binding:"required" json:"name"`
|
||||
Email string `binding:"required" json:"email"`
|
||||
Password string `binding:"required" json:"password"`
|
||||
}
|
||||
|
||||
func (u *User) New() error {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
fmt.Println("Something went wrong trying to hash a user password")
|
||||
return err
|
||||
}
|
||||
u.Password = string(hashedPassword)
|
||||
|
||||
insertQuery := `INSERT INTO users (name, email, password)
|
||||
VALUES (?, ?, ?)`
|
||||
|
||||
_, err = db.Conn.Exec(insertQuery, u.Name, u.Email, u.Password)
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindOneByEmail(email string) (*User, error) {
|
||||
selectQuery := `SELECT * FROM users WHERE email = ?`
|
||||
|
||||
dbResponse, err := db.Conn.Query(selectQuery, email)
|
||||
if err != nil {
|
||||
fmt.Println("Something went wrong trying to find a user")
|
||||
return nil, err
|
||||
}
|
||||
if dbResponse.Next() {
|
||||
var user User
|
||||
err = dbResponse.Scan(&user.ID, &user.Name, &user.Email, &user.Password)
|
||||
if err != nil {
|
||||
fmt.Println("Something went wrong parsing the database row")
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (u *User) GetJWToken() (string, error) {
|
||||
expirationDelta, err := time.ParseDuration(os.Getenv("AUTH_EXPIRATION"))
|
||||
if err != nil {
|
||||
fmt.Println("Something went wrong trying to parse AUTH_EXPIRATION env variable")
|
||||
return "", err
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": u.ID,
|
||||
"exp": time.Now().Add(expirationDelta).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
|
||||
if err != nil {
|
||||
fmt.Println("Something went wrong trying to sign the token")
|
||||
return "", err
|
||||
}
|
||||
return tokenString, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"udemy_httpserver/models"
|
||||
)
|
||||
|
||||
func registerUser(c *gin.Context) {
|
||||
var user struct {
|
||||
models.User
|
||||
PasswordConfirm string `json:"password_confirm"`
|
||||
}
|
||||
err := c.ShouldBind(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
candidateUser, err := models.FindOneByEmail(strings.ToLower(user.Email))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if candidateUser != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf("user with email %s already exists", user.Email)})
|
||||
return
|
||||
}
|
||||
|
||||
if user.Password != user.PasswordConfirm {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "password_confirm must match password"})
|
||||
return
|
||||
}
|
||||
|
||||
err = user.New()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
jwt, err := setAuthCookie(c, &user.User)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user.Password = ""
|
||||
c.JSON(http.StatusCreated, gin.H{"user": user.User, "token": jwt})
|
||||
}
|
||||
|
||||
func Login(c *gin.Context) {
|
||||
var loginDTO struct {
|
||||
Email string `binding:"required" json:"email"`
|
||||
Password string `binding:"required" json:"password"`
|
||||
}
|
||||
err := c.ShouldBind(&loginDTO)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user, err := models.FindOneByEmail(strings.ToLower(loginDTO.Email))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginDTO.Password))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid password or email"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := setAuthCookie(c, user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"jwt": token})
|
||||
|
||||
}
|
||||
|
||||
func setAuthCookie(c *gin.Context, user *models.User) (string, error) {
|
||||
JWToken, err := user.GetJWToken()
|
||||
expirationDelta, err := time.ParseDuration(os.Getenv("AUTH_EXPIRATION"))
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing AUTH_EXPIRATION")
|
||||
return "", err
|
||||
}
|
||||
|
||||
cookieExpiration := time.Now().Add(expirationDelta).Unix()
|
||||
c.SetCookie("Authorization", JWToken, int(cookieExpiration), "", "", true, true)
|
||||
return JWToken, nil
|
||||
}
|
||||
|
|
@ -7,4 +7,7 @@ func RegisterRoutes(router *gin.Engine) {
|
|||
router.GET("/events", getEvents)
|
||||
router.GET("/events/:id", getSingleEvent)
|
||||
router.PUT("/events/:id", updateEvent)
|
||||
|
||||
router.POST("/auth/register", registerUser)
|
||||
router.POST("/auth/login", Login)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue