Jelajahi Sumber

simplify handler repeated code

Alexander Belanger 4 tahun lalu
induk
melakukan
8c2bb7e9ae

+ 30 - 34
api/server/handlers/handler.go

@@ -4,11 +4,14 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/internal/repository"
 )
 
 type PorterHandler interface {
 	Config() *shared.Config
+	Repo() repository.Repository
+	HandleAPIError(w http.ResponseWriter, err apierrors.RequestError)
 }
 
 type PorterHandlerWriter interface {
@@ -18,7 +21,7 @@ type PorterHandlerWriter interface {
 
 type PorterHandlerReader interface {
 	PorterHandler
-	DecodeAndValidate(w http.ResponseWriter, r *http.Request, v interface{})
+	DecodeAndValidate(w http.ResponseWriter, r *http.Request, v interface{}) bool
 }
 
 type PorterHandlerReadWriter interface {
@@ -26,43 +29,36 @@ type PorterHandlerReadWriter interface {
 	PorterHandlerReader
 }
 
-// default
-
-// type PorterHandler struct {
-// 	config           *shared.Config
-// 	decoderValidator shared.RequestDecoderValidator
-// 	writer           shared.ResultWriter
-// }
-
-// handler needs:
-// - interface for decodervalidator+writer
-// - shared configuration
-// - writer
-// - context set (user, project, etc)
-// - standard error
-
-// notes:
-// decode and validate should happen above the handler itself. the scopes and strongly typed
+type DefaultPorterHandler struct {
+	config           *shared.Config
+	decoderValidator shared.RequestDecoderValidator
+	writer           shared.ResultWriter
+}
 
-// "handlers" refer to an aggregation of application-logic. they should not contain any logic
-// for:
-// - error aggregation (so they accept api error interface)
-// - analytics
-// - authentication
-// - reading in required context (part of authentication)
+func NewDefaultPorterHandler(
+	config *shared.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) PorterHandlerReadWriter {
+	return &DefaultPorterHandler{config, decoderValidator, writer}
+}
 
-// ProjectScopedHandler()
-// - read project model from context
-// - so "Get Project" accepts a ProjectGetter, which calls readUser() and readProject()
+func (d *DefaultPorterHandler) Config() *shared.Config {
+	return d.config
+}
 
-// The errors that a handler can throw should be defined in API spec
+func (d *DefaultPorterHandler) Repo() repository.Repository {
+	return d.config.Repo
+}
 
-type UserGetter interface {
-	readUser() *models.User
+func (d *DefaultPorterHandler) HandleAPIError(w http.ResponseWriter, err apierrors.RequestError) {
+	apierrors.HandleAPIError(w, d.Config().Logger, err)
 }
 
-type ProjectGetter interface {
-	UserGetter
+func (d *DefaultPorterHandler) WriteResult(w http.ResponseWriter, v interface{}) {
+	d.writer.WriteResult(w, v)
+}
 
-	readProject() *models.Project
+func (d *DefaultPorterHandler) DecodeAndValidate(w http.ResponseWriter, r *http.Request, v interface{}) bool {
+	return d.decoderValidator.DecodeAndValidate(w, r, v)
 }

+ 14 - 12
api/server/handlers/project/create.go

@@ -3,16 +3,16 @@ package project
 import (
 	"net/http"
 
+	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 )
 
 type ProjectCreateHandler struct {
-	config           *shared.Config
-	decoderValidator shared.RequestDecoderValidator
-	writer           shared.ResultWriter
+	handlers.PorterHandlerReadWriter
 }
 
 func NewProjectCreateHandler(
@@ -20,13 +20,15 @@ func NewProjectCreateHandler(
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *ProjectCreateHandler {
-	return &ProjectCreateHandler{config, decoderValidator, writer}
+	return &ProjectCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
 }
 
 func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	request := &types.CreateProjectRequest{}
 
-	ok := p.decoderValidator.DecodeAndValidate(w, r, request)
+	ok := p.DecodeAndValidate(w, r, request)
 
 	if !ok {
 		return
@@ -40,29 +42,29 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	}
 
 	var err error
-	proj, err = CreateProjectWithUser(p.config, proj, user)
+	proj, err = CreateProjectWithUser(p.Repo().Project(), proj, user)
 
 	if err != nil {
-		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		p.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
-	p.writer.WriteResult(w, proj.ToProjectType())
+	p.WriteResult(w, proj.ToProjectType())
 }
 
 func CreateProjectWithUser(
-	config *shared.Config,
+	projectRepo repository.ProjectRepository,
 	proj *models.Project,
 	user *models.User,
 ) (*models.Project, error) {
-	proj, err := config.Repo.Project().CreateProject(proj)
+	proj, err := projectRepo.CreateProject(proj)
 
 	if err != nil {
 		return nil, err
 	}
 
 	// create a new Role with the user as the admin
-	_, err = config.Repo.Project().CreateProjectRole(proj, &models.Role{
+	_, err = projectRepo.CreateProjectRole(proj, &models.Role{
 		Role: types.Role{
 			UserID:    user.ID,
 			ProjectID: proj.ID,
@@ -75,7 +77,7 @@ func CreateProjectWithUser(
 	}
 
 	// read the project again to get the model with the role attached
-	proj, err = config.Repo.Project().ReadProject(proj.ID)
+	proj, err = projectRepo.ReadProject(proj.ID)
 
 	if err != nil {
 		return nil, err

+ 6 - 4
api/server/handlers/project/get.go

@@ -3,25 +3,27 @@ package project
 import (
 	"net/http"
 
+	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
 
 type ProjectGetHandler struct {
-	config *shared.Config
-	writer shared.ResultWriter
+	handlers.PorterHandlerWriter
 }
 
 func NewProjectGetHandler(
 	config *shared.Config,
 	writer shared.ResultWriter,
 ) *ProjectGetHandler {
-	return &ProjectGetHandler{config, writer}
+	return &ProjectGetHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
 }
 
 func (p *ProjectGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	p.writer.WriteResult(w, proj.ToProjectType())
+	p.WriteResult(w, proj.ToProjectType())
 }

+ 1 - 1
api/server/handlers/project/get_test.go

@@ -14,7 +14,7 @@ func TestGetProjectSuccessful(t *testing.T) {
 	// create a test project
 	config := apitest.LoadConfig(t)
 	user := apitest.CreateTestUser(t, config, true)
-	proj, err := project.CreateProjectWithUser(config, &models.Project{
+	proj, err := project.CreateProjectWithUser(config.Repo.Project(), &models.Project{
 		Name: "test-project",
 	}, user)
 

+ 8 - 6
api/server/handlers/project/list.go

@@ -3,6 +3,7 @@ package project
 import (
 	"net/http"
 
+	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
@@ -10,15 +11,16 @@ import (
 )
 
 type ProjectListHandler struct {
-	config *shared.Config
-	writer shared.ResultWriter
+	handlers.PorterHandlerWriter
 }
 
 func NewProjectListHandler(
 	config *shared.Config,
 	writer shared.ResultWriter,
 ) *ProjectListHandler {
-	return &ProjectListHandler{config, writer}
+	return &ProjectListHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
 }
 
 func (p *ProjectListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -26,10 +28,10 @@ func (p *ProjectListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 
 	// read all projects for this user
-	projects, err := p.config.Repo.Project().ListProjectsByUserID(user.ID)
+	projects, err := p.Config().Repo.Project().ListProjectsByUserID(user.ID)
 
 	if err != nil {
-		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		p.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -39,5 +41,5 @@ func (p *ProjectListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		res[i] = proj.ToProjectType()
 	}
 
-	p.writer.WriteResult(w, res)
+	p.WriteResult(w, res)
 }

+ 2 - 2
api/server/handlers/project/list_test.go

@@ -15,7 +15,7 @@ func TestListProjectsSuccessful(t *testing.T) {
 	// create a test project
 	config := apitest.LoadConfig(t)
 	user := apitest.CreateTestUser(t, config, true)
-	proj1, err := project.CreateProjectWithUser(config, &models.Project{
+	proj1, err := project.CreateProjectWithUser(config.Repo.Project(), &models.Project{
 		Name: "test-project",
 	}, user)
 
@@ -23,7 +23,7 @@ func TestListProjectsSuccessful(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	proj2, err := project.CreateProjectWithUser(config, &models.Project{
+	proj2, err := project.CreateProjectWithUser(config.Repo.Project(), &models.Project{
 		Name: "test-project-2",
 	}, user)
 

+ 22 - 46
api/server/handlers/user/cli_login.go

@@ -6,6 +6,7 @@ import (
 	"net/url"
 	"time"
 
+	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
@@ -15,9 +16,7 @@ import (
 )
 
 type CLILoginHandler struct {
-	config           *shared.Config
-	decoderValidator shared.RequestDecoderValidator
-	writer           shared.ResultWriter
+	handlers.PorterHandlerReader
 }
 
 func NewCLILoginHandler(
@@ -25,13 +24,15 @@ func NewCLILoginHandler(
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *CLILoginHandler {
-	return &CLILoginHandler{config, decoderValidator, writer}
+	return &CLILoginHandler{
+		PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
 }
 
 func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	request := &types.CLILoginUserRequest{}
 
-	ok := c.decoderValidator.DecodeAndValidate(w, r, request)
+	ok := c.DecodeAndValidate(w, r, request)
 
 	if !ok {
 		return
@@ -43,30 +44,16 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	jwt, err := token.GetTokenForUser(user.ID)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				fmt.Errorf("CLI token creation failed: %s", err.Error()),
-			),
-		)
-
+		err = fmt.Errorf("CLI token creation failed: %s", err.Error())
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
-	encoded, err := jwt.EncodeToken(&token.TokenGeneratorConf{
-		TokenSecret: c.config.ServerConf.TokenGeneratorSecret,
-	})
+	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				fmt.Errorf("CLI token encoding failed: %s", err.Error()),
-			),
-		)
-
+		err = fmt.Errorf("CLI token encoding failed: %s", err.Error())
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -74,14 +61,8 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	code, err := repository.GenerateRandomBytes(32)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				fmt.Errorf("CLI random code generation failed: %s", err.Error()),
-			),
-		)
-
+		err = fmt.Errorf("CLI random code generation failed: %s", err.Error())
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -94,15 +75,10 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		Expiry:            &expiry,
 	}
 
-	authCode, err = c.config.Repo.AuthCode().CreateAuthCode(authCode)
+	authCode, err = c.Repo().AuthCode().CreateAuthCode(authCode)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(err),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -110,9 +86,7 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 }
 
 type CLILoginExchangeHandler struct {
-	config           *shared.Config
-	decoderValidator shared.RequestDecoderValidator
-	writer           shared.ResultWriter
+	handlers.PorterHandlerReadWriter
 }
 
 func NewCLILoginExchangeHandler(
@@ -120,20 +94,22 @@ func NewCLILoginExchangeHandler(
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *CLILoginExchangeHandler {
-	return &CLILoginExchangeHandler{config, decoderValidator, writer}
+	return &CLILoginExchangeHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
 }
 
 func (c *CLILoginExchangeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	request := &types.CLILoginExchangeRequest{}
 
-	ok := c.decoderValidator.DecodeAndValidate(w, r, request)
+	ok := c.DecodeAndValidate(w, r, request)
 
 	if !ok {
 		return
 	}
 
 	// look up the auth code and exchange it for a token
-	authCode, err := c.config.Repo.AuthCode().ReadAuthCode(request.AuthorizationCode)
+	authCode, err := c.Repo().AuthCode().ReadAuthCode(request.AuthorizationCode)
 
 	if err != nil || authCode.IsExpired() {
 		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
@@ -144,5 +120,5 @@ func (c *CLILoginExchangeHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		Token: authCode.Token,
 	}
 
-	c.writer.WriteResult(w, res)
+	c.WriteResult(w, res)
 }

+ 17 - 22
api/server/handlers/user/create.go

@@ -5,6 +5,7 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authn"
+	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
@@ -14,9 +15,7 @@ import (
 )
 
 type UserCreateHandler struct {
-	config           *shared.Config
-	decoderValidator shared.RequestDecoderValidator
-	writer           shared.ResultWriter
+	handlers.PorterHandlerReadWriter
 }
 
 func NewUserCreateHandler(
@@ -24,13 +23,15 @@ func NewUserCreateHandler(
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserCreateHandler {
-	return &UserCreateHandler{config, decoderValidator, writer}
+	return &UserCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
 }
 
 func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	request := &types.CreateUserRequest{}
 
-	ok := u.decoderValidator.DecodeAndValidate(w, r, request)
+	ok := u.DecodeAndValidate(w, r, request)
 
 	if !ok {
 		return
@@ -42,17 +43,11 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// check if user exists
-	doesExist := doesUserExist(u.config.Repo, user)
+	doesExist := doesUserExist(u.Repo().User(), user)
 
 	if doesExist {
-		apierrors.HandleAPIError(
-			w,
-			u.config.Logger,
-			apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("email already taken"),
-				http.StatusBadRequest,
-			),
-		)
+		err := fmt.Errorf("email already taken")
+		u.HandleAPIError(w, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
 
@@ -60,31 +55,31 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	hashedPw, err := bcrypt.GenerateFromPassword([]byte(user.Password), 8)
 
 	if err != nil {
-		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+		u.HandleAPIError(w, 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.Repo().User().CreateUser(user)
 
 	if err != nil {
-		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+		u.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	// save the user as authenticated in the session
-	if err := authn.SaveUserAuthenticated(w, r, u.config, user); err != nil {
-		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+	if err := authn.SaveUserAuthenticated(w, r, u.Config(), user); err != nil {
+		u.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
-	u.writer.WriteResult(w, user.ToUserType())
+	u.WriteResult(w, user.ToUserType())
 }
 
-func doesUserExist(repo repository.Repository, user *models.User) bool {
-	user, err := repo.User().ReadUserByEmail(user.Email)
+func doesUserExist(userRepo repository.UserRepository, user *models.User) bool {
+	user, err := userRepo.ReadUserByEmail(user.Email)
 
 	return user != nil && err == nil
 }

+ 6 - 4
api/server/handlers/user/current.go

@@ -3,25 +3,27 @@ package user
 import (
 	"net/http"
 
+	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
 
 type UserGetCurrentHandler struct {
-	config *shared.Config
-	writer shared.ResultWriter
+	handlers.PorterHandlerWriter
 }
 
 func NewUserGetCurrentHandler(
 	config *shared.Config,
 	writer shared.ResultWriter,
 ) *UserGetCurrentHandler {
-	return &UserGetCurrentHandler{config, writer}
+	return &UserGetCurrentHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
 }
 
 func (a *UserGetCurrentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 
-	a.writer.WriteResult(w, user.ToUserType())
+	a.WriteResult(w, user.ToUserType())
 }

+ 10 - 8
api/server/handlers/user/delete.go

@@ -4,6 +4,7 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authn"
+	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
@@ -11,32 +12,33 @@ import (
 )
 
 type UserDeleteHandler struct {
-	config *shared.Config
-	writer shared.ResultWriter
+	handlers.PorterHandlerWriter
 }
 
 func NewUserDeleteHandler(
 	config *shared.Config,
 	writer shared.ResultWriter,
 ) *UserDeleteHandler {
-	return &UserDeleteHandler{config, writer}
+	return &UserDeleteHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
 }
 
 func (u *UserDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 
-	user, err := u.config.Repo.User().DeleteUser(user)
+	user, err := u.Repo().User().DeleteUser(user)
 
 	if err != nil {
-		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+		u.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	// set the user as unauthenticated in the session
-	if err := authn.SaveUserUnauthenticated(w, r, u.config); err != nil {
-		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+	if err := authn.SaveUserUnauthenticated(w, r, u.Config()); err != nil {
+		u.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
-	u.writer.WriteResult(w, user.ToUserType())
+	u.WriteResult(w, user.ToUserType())
 }

+ 52 - 150
api/server/handlers/user/pw_reset.go

@@ -6,20 +6,20 @@ import (
 	"net/url"
 	"time"
 
+	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/random"
+	"github.com/porter-dev/porter/internal/repository"
 	"golang.org/x/crypto/bcrypt"
 	"gorm.io/gorm"
 )
 
 type UserPasswordInitiateResetHandler struct {
-	config           *shared.Config
-	decoderValidator shared.RequestDecoderValidator
-	writer           shared.ResultWriter
+	handlers.PorterHandlerReader
 }
 
 func NewUserPasswordInitiateResetHandler(
@@ -27,55 +27,42 @@ func NewUserPasswordInitiateResetHandler(
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserPasswordInitiateResetHandler {
-	return &UserPasswordInitiateResetHandler{config, decoderValidator, writer}
+	return &UserPasswordInitiateResetHandler{
+		PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
 }
 
 func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	request := &types.InitiateResetUserPasswordRequest{}
 
-	ok := c.decoderValidator.DecodeAndValidate(w, r, request)
+	ok := c.DecodeAndValidate(w, r, request)
 
 	if !ok {
 		return
 	}
 
 	// check that the email exists; return 200 status code even if it doesn't
-	user, err := c.config.Repo.User().ReadUserByEmail(request.Email)
+	user, err := c.Repo().User().ReadUserByEmail(request.Email)
 
 	if err == gorm.ErrRecordNotFound {
 		w.WriteHeader(http.StatusOK)
-
 		return
 	} else if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				err,
-			),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	// if the user is a Github user, send them a Github email
 	if user.GithubUserID != 0 {
-		err := c.config.UserNotifier.SendGithubRelinkEmail(
+		err := c.Config().UserNotifier.SendGithubRelinkEmail(
 			&notifier.SendGithubRelinkEmailOpts{
 				Email: user.Email,
-				URL:   fmt.Sprintf("%s/api/oauth/login/github", c.config.ServerConf.ServerURL),
+				URL:   fmt.Sprintf("%s/api/oauth/login/github", c.Config().ServerConf.ServerURL),
 			},
 		)
 
 		if err != nil {
-			apierrors.HandleAPIError(
-				w,
-				c.config.Logger,
-				apierrors.NewErrInternal(
-					err,
-				),
-			)
-
+			c.HandleAPIError(w, apierrors.NewErrInternal(err))
 			return
 		}
 
@@ -89,28 +76,14 @@ func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	rawToken, err := random.StringWithCharset(32, "")
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				err,
-			),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	hashedToken, err := bcrypt.GenerateFromPassword([]byte(rawToken), 8)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				err,
-			),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -122,17 +95,10 @@ func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	}
 
 	// handle write to the database
-	pwReset, err = c.config.Repo.PWResetToken().CreatePWResetToken(pwReset)
+	pwReset, err = c.Repo().PWResetToken().CreatePWResetToken(pwReset)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				err,
-			),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -142,22 +108,15 @@ func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 		"token_id": []string{fmt.Sprintf("%d", pwReset.ID)},
 	}
 
-	err = c.config.UserNotifier.SendPasswordResetEmail(
+	err = c.Config().UserNotifier.SendPasswordResetEmail(
 		&notifier.SendPasswordResetEmailOpts{
 			Email: user.Email,
-			URL:   fmt.Sprintf("%s/password/reset/finalize?%s", c.config.ServerConf.ServerURL, queryVals.Encode()),
+			URL:   fmt.Sprintf("%s/password/reset/finalize?%s", c.Config().ServerConf.ServerURL, queryVals.Encode()),
 		},
 	)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				err,
-			),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -166,9 +125,7 @@ func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 }
 
 type UserPasswordVerifyResetHandler struct {
-	config           *shared.Config
-	decoderValidator shared.RequestDecoderValidator
-	writer           shared.ResultWriter
+	handlers.PorterHandlerReader
 }
 
 func NewUserPasswordVerifyResetHandler(
@@ -176,19 +133,21 @@ func NewUserPasswordVerifyResetHandler(
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserPasswordVerifyResetHandler {
-	return &UserPasswordVerifyResetHandler{config, decoderValidator, writer}
+	return &UserPasswordVerifyResetHandler{
+		PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
 }
 
 func (c *UserPasswordVerifyResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	request := &types.VerifyResetUserPasswordRequest{}
 
-	ok := c.decoderValidator.DecodeAndValidate(w, r, request)
+	ok := c.DecodeAndValidate(w, r, request)
 
 	if !ok {
 		return
 	}
 
-	ok, _ = VerifyPasswordResetToken(c.config, w, request)
+	ok, _ = VerifyPasswordResetToken(c.Repo().PWResetToken(), c.HandleAPIError, w, request)
 
 	if ok {
 		w.WriteHeader(http.StatusOK)
@@ -198,9 +157,7 @@ func (c *UserPasswordVerifyResetHandler) ServeHTTP(w http.ResponseWriter, r *htt
 }
 
 type UserPasswordFinalizeResetHandler struct {
-	config           *shared.Config
-	decoderValidator shared.RequestDecoderValidator
-	writer           shared.ResultWriter
+	handlers.PorterHandlerReader
 }
 
 func NewUserPasswordFinalizeResetHandler(
@@ -208,44 +165,35 @@ func NewUserPasswordFinalizeResetHandler(
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserPasswordFinalizeResetHandler {
-	return &UserPasswordFinalizeResetHandler{config, decoderValidator, writer}
+	return &UserPasswordFinalizeResetHandler{
+		PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
 }
 
 func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	request := &types.FinalizeResetUserPasswordRequest{}
 
-	ok := c.decoderValidator.DecodeAndValidate(w, r, request)
+	ok := c.DecodeAndValidate(w, r, request)
 
 	if !ok {
 		return
 	}
 
-	ok, token := VerifyPasswordResetToken(c.config, w, &request.VerifyResetUserPasswordRequest)
+	ok, token := VerifyPasswordResetToken(c.Repo().PWResetToken(), c.HandleAPIError, w, &request.VerifyResetUserPasswordRequest)
 
 	if ok {
 		w.WriteHeader(http.StatusOK)
 	}
 
 	// check that the email exists
-	user, err := c.config.Repo.User().ReadUserByEmail(request.Email)
+	user, err := c.Repo().User().ReadUserByEmail(request.Email)
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(
-				w,
-				c.config.Logger,
-				apierrors.NewErrForbidden(
-					fmt.Errorf("finalize password reset failed: email does not exist"),
-				),
-			)
+			err = fmt.Errorf("finalize password reset failed: email does not exist")
+			c.HandleAPIError(w, apierrors.NewErrForbidden(err))
 		} else {
-			apierrors.HandleAPIError(
-				w,
-				c.config.Logger,
-				apierrors.NewErrInternal(
-					err,
-				),
-			)
+			c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		}
 
 		return
@@ -254,47 +202,26 @@ func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	hashedPW, err := bcrypt.GenerateFromPassword([]byte(request.NewPassword), 8)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				err,
-			),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	user.Password = string(hashedPW)
 
-	user, err = c.config.Repo.User().UpdateUser(user)
+	user, err = c.Repo().User().UpdateUser(user)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				err,
-			),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	// invalidate the token
 	token.IsValid = false
 
-	_, err = c.config.Repo.PWResetToken().UpdatePWResetToken(token)
+	_, err = c.Repo().PWResetToken().UpdatePWResetToken(token)
 
 	if err != nil {
-		apierrors.HandleAPIError(
-			w,
-			c.config.Logger,
-			apierrors.NewErrInternal(
-				err,
-			),
-		)
-
+		c.HandleAPIError(w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -303,29 +230,19 @@ func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 }
 
 func VerifyPasswordResetToken(
-	config *shared.Config,
+	pwResetRepo repository.PWResetTokenRepository,
+	handleErr func(w http.ResponseWriter, apiErr apierrors.RequestError),
 	w http.ResponseWriter,
 	request *types.VerifyResetUserPasswordRequest,
 ) (bool, *models.PWResetToken) {
-	token, err := config.Repo.PWResetToken().ReadPWResetToken(request.TokenID)
+	token, err := pwResetRepo.ReadPWResetToken(request.TokenID)
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(
-				w,
-				config.Logger,
-				apierrors.NewErrForbidden(
-					fmt.Errorf("verify/finalize password reset failed: token does not exist"),
-				),
-			)
+			err = fmt.Errorf("verify/finalize password reset failed: token does not exist")
+			handleErr(w, apierrors.NewErrForbidden(err))
 		} else {
-			apierrors.HandleAPIError(
-				w,
-				config.Logger,
-				apierrors.NewErrInternal(
-					err,
-				),
-			)
+			handleErr(w, apierrors.NewErrInternal(err))
 		}
 
 		return false, nil
@@ -333,39 +250,24 @@ func VerifyPasswordResetToken(
 
 	// make sure the token is still valid and has not expired
 	if !token.IsValid || token.IsExpired() {
-		apierrors.HandleAPIError(
-			w,
-			config.Logger,
-			apierrors.NewErrForbidden(
-				fmt.Errorf("verify password reset failed: expired %t, valid %t", token.IsExpired(), token.IsValid),
-			),
-		)
+		err = fmt.Errorf("verify password reset failed: expired %t, valid %t", token.IsExpired(), token.IsValid)
+		handleErr(w, apierrors.NewErrForbidden(err))
 
 		return false, nil
 	}
 
 	// check that the email matches
 	if token.Email != request.Email {
-		apierrors.HandleAPIError(
-			w,
-			config.Logger,
-			apierrors.NewErrForbidden(
-				fmt.Errorf("verify password reset failed: token email does not match request email"),
-			),
-		)
+		err = fmt.Errorf("verify password reset failed: token email does not match request email")
+		handleErr(w, apierrors.NewErrForbidden(err))
 
 		return false, nil
 	}
 
 	// make sure the token is correct
 	if err := bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(request.Token)); err != nil {
-		apierrors.HandleAPIError(
-			w,
-			config.Logger,
-			apierrors.NewErrForbidden(
-				fmt.Errorf("verify password reset failed: %s", err),
-			),
-		)
+		err = fmt.Errorf("verify password reset failed: %s", err)
+		handleErr(w, apierrors.NewErrForbidden(err))
 
 		return false, nil
 	}