From 5c0c229d523977108f8a963d46982fc4037cfeab Mon Sep 17 00:00:00 2001 From: cukhoaimon Date: Thu, 1 Feb 2024 16:44:47 +0700 Subject: [PATCH] feat: add user api --- api/server.go | 2 ++ api/user.go | 69 ++++++++++++++++++++++++++++++++++++++++ db/sqlc/user.sql_test.go | 5 ++- utils/password.go | 21 ++++++++++++ utils/password_test.go | 47 +++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 api/user.go create mode 100644 utils/password.go create mode 100644 utils/password_test.go diff --git a/api/server.go b/api/server.go index 1365624..dc95db4 100644 --- a/api/server.go +++ b/api/server.go @@ -27,6 +27,8 @@ func NewServer(store db.Store) *Server { } // routing here + router.POST("/api/v1/user", server.createUser) + router.GET("/api/v1/account", server.listAccount) router.GET("/api/v1/account/:id", server.getAccount) router.POST("/api/v1/account", server.createAccount) diff --git a/api/user.go b/api/user.go new file mode 100644 index 0000000..4f578ca --- /dev/null +++ b/api/user.go @@ -0,0 +1,69 @@ +package api + +import ( + "errors" + db "github.com/cukhoaimon/SimpleBank/db/sqlc" + "github.com/cukhoaimon/SimpleBank/utils" + "github.com/gin-gonic/gin" + "github.com/lib/pq" + "net/http" + "time" +) + +type createUserRequest struct { + Username string `json:"username" binding:"required,alphanum"` + Password string `json:"password" binding:"required,min=8"` + FullName string `json:"full_name" binding:"required"` + Email string `json:"email" binding:"required,email"` +} + +type createUserResponse struct { + Username string `json:"username"` + FullName string `json:"full_name"` + Email string `json:"email"` + PasswordChangedAt time.Time `json:"password_changed_at"` + CreatedAt time.Time `json:"created_at"` +} + +func (server *Server) createUser(ctx *gin.Context) { + var req createUserRequest + + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, errorResponse(err)) + return + } + + hashedPassword, err := utils.HashPassword(req.Password) + + arg := db.CreateUserParams{ + Username: req.Username, + HashedPassword: hashedPassword, + FullName: req.FullName, + Email: req.Email, + } + + user, err := server.store.CreateUser(ctx, arg) + if err != nil { + var pqErr *pq.Error + if errors.As(err, &pqErr) { + switch pqErr.Code.Name() { + case "unique_violation": + ctx.JSON(http.StatusForbidden, errorResponse(err)) + return + } + } + + ctx.JSON(http.StatusInternalServerError, errorResponse(err)) + return + } + + rsp := createUserResponse{ + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + PasswordChangedAt: user.PasswordChangedAt, + CreatedAt: user.CreatedAt, + } + + ctx.JSON(http.StatusCreated, rsp) +} diff --git a/db/sqlc/user.sql_test.go b/db/sqlc/user.sql_test.go index d3578a3..70cabfc 100644 --- a/db/sqlc/user.sql_test.go +++ b/db/sqlc/user.sql_test.go @@ -8,9 +8,12 @@ import ( ) func createRandomUser(t *testing.T) User { + hashedPassword, err := utils.HashPassword(utils.RandomString(10)) + require.Nil(t, err) + arg := CreateUserParams{ Username: utils.RandomOwner(), - HashedPassword: "secret", + HashedPassword: hashedPassword, FullName: utils.RandomOwner(), Email: utils.RandomEmail(), } diff --git a/utils/password.go b/utils/password.go new file mode 100644 index 0000000..9893c6b --- /dev/null +++ b/utils/password.go @@ -0,0 +1,21 @@ +package utils + +import ( + "fmt" + "golang.org/x/crypto/bcrypt" +) + +// HashPassword returns the password hashed by bcrypt +func HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", fmt.Errorf("fail to hash password: %w", err) + } + + return string(hashedPassword), nil +} + +// CheckPassword check if the password is match or not +func CheckPassword(password, hashedPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} diff --git a/utils/password_test.go b/utils/password_test.go new file mode 100644 index 0000000..e57e6a3 --- /dev/null +++ b/utils/password_test.go @@ -0,0 +1,47 @@ +package utils + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestPassword(t *testing.T) { + password := RandomString(10) + hashedPassword, err := HashPassword(password) + + require.Nil(t, err) + + type args struct { + password string + hashedPassword string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Same password", + args: args{ + password: password, + hashedPassword: hashedPassword, + }, + wantErr: false, + }, + { + name: "Different password", + args: args{ + password: RandomString(10), + hashedPassword: hashedPassword, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CheckPassword(tt.args.password, tt.args.hashedPassword); (err != nil) != tt.wantErr { + t.Errorf("CheckPassword() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}