diff --git a/api/server.go b/api/server.go index 0fa188f..bf7cbf1 100644 --- a/api/server.go +++ b/api/server.go @@ -21,6 +21,7 @@ func NewServer(store db.Store) *Server { v.RegisterValidation("currency", validCurrency) } + router.POST("/users", server.createUser) router.POST("/accounts", server.createAccount) router.GET("/accounts/:id", server.getAccount) router.GET("/accounts", server.listAccount) diff --git a/api/user.go b/api/user.go new file mode 100644 index 0000000..1ed936d --- /dev/null +++ b/api/user.go @@ -0,0 +1,71 @@ +package api + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/lib/pq" + db "github.com/sinachaichi/gault/db/sqlc" + "github.com/sinachaichi/gault/util" +) + +type createUserRequest struct { + Username string `json:"username" binding:"required,alphanum"` + Password string `json:"password" binding:"required,min=6"` + 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 := util.HashPassword(req.Password) + if err != nil { + ctx.JSON(http.StatusInternalServerError, errorResponse(err)) + return + } + + arg := db.CreateUserParams{ + Username: req.Username, + HashedPassword: hashedPassword, + FullName: req.FullName, + Email: req.Email, + } + + + user, err := server.store.CreateUser(ctx, arg) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + 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.StatusOK, rsp) +} \ No newline at end of file diff --git a/db/sqlc/user_test.go b/db/sqlc/user_test.go index a81e88b..bedc505 100644 --- a/db/sqlc/user_test.go +++ b/db/sqlc/user_test.go @@ -11,9 +11,12 @@ import ( ) func createRandomUser(t *testing.T) User { + hashedPassword, err := util.HashPassword(util.RandomString(6)) + require.NoError(t, err) + arg := CreateUserParams{ Username: util.RandomOwner(), - HashedPassword: "secret", + HashedPassword: hashedPassword, FullName: util.RandomOwner(), Email: util.RandomEmail(), } diff --git a/util/passowrd_test.go b/util/passowrd_test.go new file mode 100644 index 0000000..e7d3e85 --- /dev/null +++ b/util/passowrd_test.go @@ -0,0 +1,34 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/crypto/bcrypt" +) + + +func TestPassword(t *testing.T) { + password := RandomString(6) + + hashedPassword1, err := HashPassword(password) + require.NoError(t, err) + require.NotEmpty(t, hashedPassword1) + + err = CheckPassword(password, hashedPassword1) + require.NoError(t, err) + + wrongPassword := RandomString(6) + err = CheckPassword(wrongPassword, hashedPassword1) + require.EqualError(t, err, bcrypt.ErrMismatchedHashAndPassword.Error()) + + hashedPassword2, err := HashPassword(password) + require.NoError(t, err) + require.NotEmpty(t, hashedPassword2) + require.NotEqual(t, hashedPassword1, hashedPassword2) + + longPassword := RandomString(73) + hashedLongPassword, err := HashPassword(longPassword) + require.Error(t, err) + require.Empty(t, hashedLongPassword) +} \ No newline at end of file diff --git a/util/password.go b/util/password.go new file mode 100644 index 0000000..b722474 --- /dev/null +++ b/util/password.go @@ -0,0 +1,21 @@ +package util + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + + +func HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", fmt.Errorf("failed to hash password: %w", err) + } + return string(hashedPassword), nil +} + +func CheckPassword(password string, hashedPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} +