Ver Fonte

add login user endpoint

Alexander Belanger há 4 anos atrás
pai
commit
cc76acb1b7

+ 12 - 1
api/server/handlers/user/create.go

@@ -10,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/crypto/bcrypt"
 )
 
 type UserCreateHandler struct {
@@ -55,8 +56,18 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// hash the password using bcrypt
+	hashedPw, err := bcrypt.GenerateFromPassword([]byte(user.Password), 8)
+
+	if err != nil {
+		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
+
+	user.Password = string(hashedPw)
+
 	// write the user to the db
-	user, err := u.config.Repo.User().CreateUser(user)
+	user, err = u.config.Repo.User().CreateUser(user)
 
 	if err != nil {
 		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))

+ 73 - 0
api/server/handlers/user/login.go

@@ -0,0 +1,73 @@
+package user
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authn"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/types"
+	"golang.org/x/crypto/bcrypt"
+	"gorm.io/gorm"
+)
+
+type UserLoginHandler struct {
+	config           *shared.Config
+	decoderValidator shared.RequestDecoderValidator
+	writer           shared.ResultWriter
+}
+
+func NewUserLoginHandler(
+	config *shared.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UserLoginHandler {
+	return &UserLoginHandler{config, decoderValidator, writer}
+}
+
+func (u *UserLoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	request := &types.LoginUserRequest{}
+
+	ok := u.decoderValidator.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	// check that passwords match
+	storedUser, err := u.config.Repo.User().ReadUserByEmail(request.Email)
+
+	// case on user not existing, send forbidden error if not exist
+	if err != nil {
+		if targetErr := gorm.ErrRecordNotFound; errors.Is(err, targetErr) {
+			apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrForbidden(err))
+			return
+		} else {
+			apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	if err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(request.Password)); err != nil {
+		apierrors.HandleAPIError(
+			w,
+			u.config.Logger,
+			apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("incorrect password"),
+				http.StatusUnauthorized,
+			),
+		)
+
+		return
+	}
+
+	// save the user as authenticated in the session
+	if err := authn.SaveUserAuthenticated(w, r, u.config, storedUser); err != nil {
+		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
+
+	u.writer.WriteResult(w, storedUser.ToUserType())
+}

+ 177 - 0
api/server/handlers/user/login_test.go

@@ -0,0 +1,177 @@
+package user_test
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/porter-dev/porter/api/server/handlers/user"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apitest"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/repository/test"
+)
+
+func TestLoginUserSuccessful(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users/login",
+		&types.LoginUserRequest{
+			Email:    "test@test.it",
+			Password: "hello",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+	apitest.CreateTestUser(t, config)
+
+	handler := user.NewUserLoginHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	expUser := &types.LoginUserResponse{
+		ID:            1,
+		Email:         "test@test.it",
+		EmailVerified: true,
+	}
+
+	gotUser := &types.LoginUserResponse{}
+
+	apitest.AssertResponseExpected(t, rr, expUser, gotUser)
+}
+
+func TestLoginUserIncorrectPassword(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users/login",
+		&types.LoginUserRequest{
+			Email:    "test@test.it",
+			Password: "hello1",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+	apitest.CreateTestUser(t, config)
+
+	handler := user.NewUserLoginHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseError(t, rr, http.StatusUnauthorized, &types.ExternalError{
+		Error: fmt.Sprintf("incorrect password"),
+	})
+}
+
+func TestLoginUserBadEmail(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users/login",
+		&types.LoginUserRequest{
+			Email:    "test",
+			Password: "hello1",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+	apitest.CreateTestUser(t, config)
+
+	handler := user.NewUserLoginHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseError(t, rr, http.StatusBadRequest, &types.ExternalError{
+		Error: fmt.Sprintf("validation failed on field 'Email' on condition 'email'"),
+	})
+}
+
+func TestLoginUserEmptyPassword(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users/login",
+		&types.LoginUserRequest{
+			Email:    "test@test.it",
+			Password: "",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+	apitest.CreateTestUser(t, config)
+
+	handler := user.NewUserLoginHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseError(t, rr, http.StatusBadRequest, &types.ExternalError{
+		Error: fmt.Sprintf("validation failed on field 'Password' on condition 'required'"),
+	})
+}
+
+func TestLoginUserNotExist(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users/login",
+		&types.LoginUserRequest{
+			Email:    "test@example.com",
+			Password: "hello",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+	apitest.CreateTestUser(t, config)
+
+	handler := user.NewUserLoginHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseForbidden(t, rr)
+}
+
+func TestLoginUserFailingReadUserByEmailMethod(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users/login",
+		&types.LoginUserRequest{
+			Email:    "test@test.it",
+			Password: "hello",
+		},
+	)
+
+	config := apitest.LoadConfig(t, test.ReadUserByEmailMethod)
+	apitest.CreateTestUser(t, config)
+
+	handler := user.NewUserLoginHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseInternalServerError(t, rr)
+}

+ 24 - 0
api/server/router/base.go

@@ -47,5 +47,29 @@ func GetBaseRoutes(
 		Router:   r,
 	})
 
+	// POST /api/login -> user.NewUserLoginHandler
+	loginUserEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/login",
+			},
+		},
+	)
+
+	loginUserHandler := user.NewUserLoginHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: loginUserEndpoint,
+		Handler:  loginUserHandler,
+		Router:   r,
+	})
+
 	return routes
 }

+ 4 - 1
api/server/shared/apitest/user.go

@@ -5,12 +5,15 @@ import (
 
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/internal/models"
+	"golang.org/x/crypto/bcrypt"
 )
 
 func CreateTestUser(t *testing.T, config *shared.Config) *models.User {
+	hashedPw, _ := bcrypt.GenerateFromPassword([]byte("hello"), 8)
+
 	user, err := config.Repo.User().CreateUser(&models.User{
 		Email:         "test@test.it",
-		Password:      "hello",
+		Password:      string(hashedPw),
 		EmailVerified: true,
 	})
 

+ 7 - 0
api/types/user.go

@@ -14,3 +14,10 @@ type CreateUserRequest struct {
 type CreateUserResponse User
 
 type GetAuthenticatedUserResponse User
+
+type LoginUserRequest struct {
+	Email    string `json:"email" form:"required,max=255,email"`
+	Password string `json:"password" form:"required,max=255"`
+}
+
+type LoginUserResponse User

+ 6 - 4
internal/repository/test/user.go

@@ -11,8 +11,10 @@ import (
 )
 
 const (
-	CreateUserMethod string = "create_user_0"
-	DeleteUserMethod string = "delete_user_0"
+	CreateUserMethod      string = "create_user_0"
+	ReadUserMethod        string = "read_user_0"
+	ReadUserByEmailMethod string = "read_user_by_email_0"
+	DeleteUserMethod      string = "delete_user_0"
 )
 
 // UserRepository will return errors on queries if canQuery is false
@@ -51,7 +53,7 @@ func (repo *UserRepository) CreateUser(user *models.User) (*models.User, error)
 
 // ReadUser finds a single user based on their unique id
 func (repo *UserRepository) ReadUser(id uint) (*models.User, error) {
-	if !repo.canQuery {
+	if !repo.canQuery || strings.Contains(repo.failingMethods, ReadUserMethod) {
 		return nil, errors.New("Cannot read from database")
 	}
 
@@ -65,7 +67,7 @@ func (repo *UserRepository) ReadUser(id uint) (*models.User, error) {
 
 // ReadUserByEmail finds a single user based on their unique email
 func (repo *UserRepository) ReadUserByEmail(email string) (*models.User, error) {
-	if !repo.canQuery {
+	if !repo.canQuery || strings.Contains(repo.failingMethods, ReadUserByEmailMethod) {
 		return nil, errors.New("Cannot read from database")
 	}