Bladeren bron

[wip] get example create project route finished

Alexander Belanger 5 jaren geleden
bovenliggende
commit
7859ac3943

+ 13 - 124
api/server/authn/handler_test.go

@@ -9,9 +9,8 @@ import (
 
 	"github.com/porter-dev/porter/api/server/authn"
 	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/test"
+	"github.com/porter-dev/porter/api/server/shared/apitest"
 	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/stretchr/testify/assert"
 	"gorm.io/gorm"
@@ -29,17 +28,8 @@ func TestAuthenticatedUserWithCookie(t *testing.T) {
 	rr := httptest.NewRecorder()
 
 	// create a new user and a cookie for them
-	user, err := config.Repo.User().CreateUser(&models.User{
-		Email:         "test@test.it",
-		Password:      "hello",
-		EmailVerified: true,
-	})
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	cookie := authenticateUserWithCookie(t, config, user, false)
+	user := apitest.CreateTestUser(t, config)
+	cookie := apitest.AuthenticateUserWithCookie(t, config, user, false)
 	req.AddCookie(cookie)
 
 	handler.ServeHTTP(rr, req)
@@ -76,17 +66,8 @@ func TestAuthenticatedUserWithToken(t *testing.T) {
 	rr := httptest.NewRecorder()
 
 	// create a new user for the token to reference
-	user, err := config.Repo.User().CreateUser(&models.User{
-		Email:         "test@test.it",
-		Password:      "hello",
-		EmailVerified: true,
-	})
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	tokenStr := authenticateUserWithToken(t, config, user.ID)
+	user := apitest.CreateTestUser(t, config)
+	tokenStr := apitest.AuthenticateUserWithToken(t, config, user.ID)
 	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tokenStr))
 
 	handler.ServeHTTP(rr, req)
@@ -126,21 +107,12 @@ func TestAuthBadDatabaseRead(t *testing.T) {
 	rr := httptest.NewRecorder()
 
 	// create a new user and a cookie for them
-	user, err := config.Repo.User().CreateUser(&models.User{
-		Email:         "test@test.it",
-		Password:      "hello",
-		EmailVerified: true,
-	})
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	cookie := authenticateUserWithCookie(t, config, user, false)
+	user := apitest.CreateTestUser(t, config)
+	cookie := apitest.AuthenticateUserWithCookie(t, config, user, false)
 	req.AddCookie(cookie)
 
 	// set the repository interface to one that can't query from the db
-	configLoader := test.NewTestConfigLoader(false)
+	configLoader := apitest.NewTestConfigLoader(false)
 	config, err = configLoader.LoadConfig()
 	factory := authn.NewAuthNFactory(config)
 	handler = factory.NewAuthenticated(next)
@@ -162,19 +134,11 @@ func TestAuthBadSessionUserWrite(t *testing.T) {
 	rr := httptest.NewRecorder()
 
 	// create a new user and a cookie for them
-	_, err = config.Repo.User().CreateUser(&models.User{
-		Email:         "test@test.it",
-		Password:      "hello",
-		EmailVerified: true,
-	})
-
-	if err != nil {
-		t.Fatal(err)
-	}
+	apitest.CreateTestUser(t, config)
 
 	// create cookie where session values are incorrect
 	// i.e. written for a user that doesn't exist (id 500)
-	cookie := authenticateUserWithCookie(t, config, &models.User{
+	cookie := apitest.AuthenticateUserWithCookie(t, config, &models.User{
 		Model: gorm.Model{
 			ID: 500,
 		},
@@ -198,19 +162,11 @@ func TestAuthBadSessionUserIDType(t *testing.T) {
 	rr := httptest.NewRecorder()
 
 	// create a new user and a cookie for them
-	user, err := config.Repo.User().CreateUser(&models.User{
-		Email:         "test@test.it",
-		Password:      "hello",
-		EmailVerified: true,
-	})
-
-	if err != nil {
-		t.Fatal(err)
-	}
+	user := apitest.CreateTestUser(t, config)
 
 	// create cookie where session values are incorrect
 	// i.e. written for a user that doesn't exist (id 500)
-	cookie := authenticateUserWithCookie(t, config, user, true)
+	cookie := apitest.AuthenticateUserWithCookie(t, config, user, true)
 
 	req.AddCookie(cookie)
 	handler.ServeHTTP(rr, req)
@@ -232,13 +188,7 @@ func (t *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 }
 
 func loadHandlers(t *testing.T) (*shared.Config, http.Handler, *testHandler) {
-	configLoader := test.NewTestConfigLoader(true)
-
-	config, err := configLoader.LoadConfig()
-
-	if err != nil {
-		t.Fatal(err)
-	}
+	config := apitest.LoadConfig(t)
 
 	factory := authn.NewAuthNFactory(config)
 
@@ -248,67 +198,6 @@ func loadHandlers(t *testing.T) (*shared.Config, http.Handler, *testHandler) {
 	return config, handler, next
 }
 
-// authenticateUserWithCookie uses the session store to create a cookie for a user
-func authenticateUserWithCookie(
-	t *testing.T,
-	config *shared.Config,
-	user *models.User,
-	badUserIDType bool,
-) *http.Cookie {
-	rr2 := httptest.NewRecorder()
-	req2, err := http.NewRequest("GET", "/login", nil)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// set the user as authenticated
-	session, err := config.Store.Get(req2, config.CookieName)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	session.Values["authenticated"] = true
-	session.Values["user_id"] = user.ID
-	session.Values["email"] = user.Email
-
-	if badUserIDType {
-		session.Values["user_id"] = "badtype"
-	}
-
-	if err := session.Save(req2, rr2); err != nil {
-		t.Fatal(err)
-	}
-
-	var cookie *http.Cookie
-
-	if cookies := rr2.Result().Cookies(); len(cookies) > 0 {
-		cookie = cookies[0]
-	} else {
-		t.Fatal(fmt.Errorf("no cookie in response"))
-	}
-
-	return cookie
-}
-
-// authenticateUserWithToken uses the JWT token generator to create a token for a user
-func authenticateUserWithToken(t *testing.T, config *shared.Config, userID uint) string {
-	issToken, err := token.GetTokenForUser(userID)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	res, err := issToken.EncodeToken(config.TokenConf)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	return res
-}
-
 func assertForbiddenError(t *testing.T, next *testHandler, rr *httptest.ResponseRecorder) {
 	assert := assert.New(t)
 

+ 55 - 10
api/server/handlers/project/create.go

@@ -4,24 +4,69 @@ import (
 	"net/http"
 
 	"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"
 )
 
 type ProjectCreateHandler struct {
-	config *shared.Config
-
-	endpoint *shared.APIEndpoint
+	config           *shared.Config
+	decoderValidator shared.RequestDecoderValidator
+	writer           shared.ResultWriter
 }
 
-func NewProjectCreateHandler(config *shared.Config, endpoint *shared.APIEndpoint) *ProjectCreateHandler {
-	return &ProjectCreateHandler{config, endpoint}
+func NewProjectCreateHandler(
+	config *shared.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ProjectCreateHandler {
+	return &ProjectCreateHandler{config, decoderValidator, writer}
 }
 
 func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// request := &types.CreateProjectRequest{}
+	request := &types.CreateProjectRequest{}
+
+	ok := p.decoderValidator.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	// read the user from context
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+	proj := &models.Project{
+		Name: request.Name,
+	}
+
+	proj, err := p.config.Repo.Project().CreateProject(proj)
+
+	if err != nil {
+		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// create a new Role with the user as the admin
+	_, err = p.config.Repo.Project().CreateProjectRole(proj, &models.Role{
+		Role: types.Role{
+			UserID:    user.ID,
+			ProjectID: proj.ID,
+			Kind:      types.RoleAdmin,
+		},
+	})
+
+	if err != nil {
+		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// read the project again to get the model with the role attached
+	proj, err = p.config.Repo.Project().ReadProject(proj.ID)
 
-	// ok := p.endpoint.Reader(r.Body, request)
+	if err != nil {
+		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
 
-	// if !ok {
-	// 	return
-	// }
+	p.writer.WriteResult(w, proj.ToProjectType())
 }

+ 87 - 0
api/server/handlers/project/create_test.go

@@ -0,0 +1,87 @@
+package project_test
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/porter-dev/porter/api/server/handlers/project"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apitest"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCreateProjectSuccessful(t *testing.T) {
+	// create request for create project
+	data, err := json.Marshal(&types.CreateProjectRequest{
+		Name: "test-project",
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, err := http.NewRequest("POST", "/api/projects", strings.NewReader(string(data)))
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	rr := httptest.NewRecorder()
+
+	// attach authenticated user to context
+	config := apitest.LoadConfig(t)
+
+	user := apitest.CreateTestUser(t, config)
+
+	ctx := req.Context()
+	ctx = context.WithValue(ctx, types.UserScope, user)
+
+	req = req.WithContext(ctx)
+
+	// create the project
+	handler := project.NewProjectCreateHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(
+			config,
+			requestutils.NewDefaultValidator(),
+			requestutils.NewDefaultDecoder(),
+		),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	// ensure the API response is correct
+	expProject := &types.CreateProjectResponse{
+		ID:   1,
+		Name: "test-project",
+		Roles: []*types.Role{
+			{
+				Kind:      types.RoleAdmin,
+				UserID:    user.ID,
+				ProjectID: 1,
+			},
+		},
+	}
+
+	gotProject := &types.CreateProjectResponse{}
+
+	err = json.NewDecoder(rr.Body).Decode(gotProject)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(
+		t,
+		expProject,
+		gotProject,
+		"incorrect response data",
+	)
+}

+ 0 - 26
api/server/router/project.go

@@ -3,7 +3,6 @@ package router
 import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/authz"
-	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/types"
 )
@@ -32,30 +31,5 @@ func registerProjectEndpoints(
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) {
-	routes := make([]*Route, 0)
 
-	projectPath := &types.Path{
-		Parent:       basePath,
-		RelativePath: "/projects",
-	}
-
-	createEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent:       projectPath,
-				RelativePath: "",
-			},
-		},
-	)
-
-	createHandler := project.NewProjectCreateHandler(config, createEndpoint)
-
-	routes = append(routes, &Route{
-		Endpoint: createEndpoint,
-		Handler:  createHandler,
-	})
-
-	registerRoutes(r, routes)
 }

+ 10 - 0
api/server/router/router.go

@@ -20,6 +20,16 @@ func NewAPIRouter(config *shared.Config) *chi.Mux {
 			// all authenticated endpoints use the authn middleware
 			r.Use(authNFactory.NewAuthenticated)
 
+			// register all user-scoped routes
+			RegisterUserScopedRoutes(
+				r,
+				config,
+				&types.Path{
+					RelativePath: "",
+				},
+				endpointFactory,
+			)
+
 			// register all project-scoped routes
 			RegisterProjectScopedRoutes(
 				r,

+ 60 - 0
api/server/router/user.go

@@ -0,0 +1,60 @@
+package router
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers/project"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func RegisterUserScopedRoutes(
+	r chi.Router,
+	config *shared.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) chi.Router {
+	// Create a new "project-scoped" factory which will create a new project-scoped request
+	// after authorization. Each subsequent http.Handler can lookup the project in context.
+	projFactory := authz.NewProjectScopedFactory(config.Repo.Project(), config)
+
+	// attach middleware to router
+	r.Use(projFactory.NewProjectScoped)
+
+	registerProjectEndpoints(r, config, basePath, factory)
+
+	return r
+}
+
+func registerUserRoutes(
+	r chi.Router,
+	config *shared.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) {
+	routes := make([]*Route, 0)
+
+	createEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/projects",
+			},
+		},
+	)
+
+	createHandler := project.NewProjectCreateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: createEndpoint,
+		Handler:  createHandler,
+	})
+
+	registerRoutes(r, routes)
+}

+ 73 - 0
api/server/shared/apitest/authn.go

@@ -0,0 +1,73 @@
+package apitest
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// AuthenticateUserWithCookie uses the session store to create a cookie for a user
+func AuthenticateUserWithCookie(
+	t *testing.T,
+	config *shared.Config,
+	user *models.User,
+	badUserIDType bool,
+) *http.Cookie {
+	rr2 := httptest.NewRecorder()
+	req2, err := http.NewRequest("GET", "/login", nil)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// set the user as authenticated
+	session, err := config.Store.Get(req2, config.CookieName)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	session.Values["authenticated"] = true
+	session.Values["user_id"] = user.ID
+	session.Values["email"] = user.Email
+
+	if badUserIDType {
+		session.Values["user_id"] = "badtype"
+	}
+
+	if err := session.Save(req2, rr2); err != nil {
+		t.Fatal(err)
+	}
+
+	var cookie *http.Cookie
+
+	if cookies := rr2.Result().Cookies(); len(cookies) > 0 {
+		cookie = cookies[0]
+	} else {
+		t.Fatal(fmt.Errorf("no cookie in response"))
+	}
+
+	return cookie
+}
+
+// AuthenticateUserWithToken uses the JWT token generator to create a token for a user
+func AuthenticateUserWithToken(t *testing.T, config *shared.Config, userID uint) string {
+	issToken, err := token.GetTokenForUser(userID)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	res, err := issToken.EncodeToken(config.TokenConf)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return res
+}

+ 14 - 1
api/server/shared/test/config.go → api/server/shared/apitest/config.go

@@ -1,7 +1,8 @@
-package test
+package apitest
 
 import (
 	"os"
+	"testing"
 
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/internal/auth/sessionstore"
@@ -41,3 +42,15 @@ func (t *TestConfigLoader) LoadConfig() (*shared.Config, error) {
 		TokenConf:  tokenConf,
 	}, nil
 }
+
+func LoadConfig(t *testing.T) *shared.Config {
+	configLoader := NewTestConfigLoader(true)
+
+	config, err := configLoader.LoadConfig()
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return config
+}

+ 22 - 0
api/server/shared/apitest/user.go

@@ -0,0 +1,22 @@
+package apitest
+
+import (
+	"testing"
+
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+func CreateTestUser(t *testing.T, config *shared.Config) *models.User {
+	user, err := config.Repo.User().CreateUser(&models.User{
+		Email:         "test@test.it",
+		Password:      "hello",
+		EmailVerified: true,
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return user
+}

+ 17 - 7
api/server/shared/endpoints.go

@@ -1,7 +1,7 @@
 package shared
 
 import (
-	"github.com/porter-dev/porter/api/server/requestutils"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -13,11 +13,13 @@ type APIEndpoint struct {
 
 type APIEndpointFactory interface {
 	NewAPIEndpoint(metadata *types.APIRequestMetadata) *APIEndpoint
+	GetDecoderValidator() RequestDecoderValidator
+	GetResultWriter() ResultWriter
 }
 
 type APIObjectEndpointFactory struct {
-	decoderValidator RequestDecoderValidator
-	resultWriter     ResultWriter
+	DecoderValidator RequestDecoderValidator
+	ResultWriter     ResultWriter
 }
 
 func NewAPIObjectEndpointFactory(config *Config) APIEndpointFactory {
@@ -28,8 +30,8 @@ func NewAPIObjectEndpointFactory(config *Config) APIEndpointFactory {
 	resultWriter := NewDefaultResultWriter(config)
 
 	return &APIObjectEndpointFactory{
-		decoderValidator: decoderValidator,
-		resultWriter:     resultWriter,
+		DecoderValidator: decoderValidator,
+		ResultWriter:     resultWriter,
 	}
 }
 
@@ -38,7 +40,15 @@ func (factory *APIObjectEndpointFactory) NewAPIEndpoint(
 ) *APIEndpoint {
 	return &APIEndpoint{
 		Metadata:         metadata,
-		DecoderValidator: factory.decoderValidator,
-		Writer:           factory.resultWriter,
+		DecoderValidator: factory.DecoderValidator,
+		Writer:           factory.ResultWriter,
 	}
 }
+
+func (factory *APIObjectEndpointFactory) GetDecoderValidator() RequestDecoderValidator {
+	return factory.DecoderValidator
+}
+
+func (factory *APIObjectEndpointFactory) GetResultWriter() ResultWriter {
+	return factory.ResultWriter
+}

+ 1 - 1
api/server/shared/reader.go

@@ -3,8 +3,8 @@ package shared
 import (
 	"net/http"
 
-	"github.com/porter-dev/porter/api/server/requestutils"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
 )
 
 type RequestDecoderValidator interface {

+ 0 - 0
api/server/requestutils/decoder.go → api/server/shared/requestutils/decoder.go


+ 1 - 1
api/server/requestutils/decoder_test.go → api/server/shared/requestutils/decoder_test.go

@@ -10,8 +10,8 @@ import (
 	"testing"
 
 	"github.com/go-test/deep"
-	"github.com/porter-dev/porter/api/server/requestutils"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/stretchr/testify/assert"
 )
 

+ 0 - 0
api/server/requestutils/validator.go → api/server/shared/requestutils/validator.go


+ 1 - 1
api/server/requestutils/validator_test.go → api/server/shared/requestutils/validator_test.go

@@ -7,8 +7,8 @@ import (
 
 	"github.com/stretchr/testify/assert"
 
-	"github.com/porter-dev/porter/api/server/requestutils"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
 )
 
 const (

+ 11 - 3
api/server/shared/writer.go

@@ -1,6 +1,11 @@
 package shared
 
-import "net/http"
+import (
+	"encoding/json"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+)
 
 type ResultWriter interface {
 	WriteResult(w http.ResponseWriter, v interface{})
@@ -17,6 +22,9 @@ func NewDefaultResultWriter(config *Config) ResultWriter {
 }
 
 func (j *DefaultResultWriter) WriteResult(w http.ResponseWriter, v interface{}) {
-	// TODO: unimplemented
-	return
+	err := json.NewEncoder(w).Encode(v)
+
+	if err != nil {
+		apierrors.HandleAPIError(w, j.config.Logger, apierrors.NewErrInternal(err))
+	}
 }

+ 6 - 13
api/types/project.go

@@ -1,15 +1,9 @@
 package types
 
 type Project struct {
-	ID    uint          `json:"id"`
-	Name  string        `json:"name"`
-	Roles []ProjectRole `json:"roles"`
-}
-
-type ProjectRole struct {
-	Kind      string `json:"kind"`
-	UserID    uint   `json:"user_id"`
-	ProjectID uint   `json:"project_id"`
+	ID    uint    `json:"id"`
+	Name  string  `json:"name"`
+	Roles []*Role `json:"roles"`
 }
 
 type CreateProjectRequest struct {
@@ -19,12 +13,11 @@ type CreateProjectRequest struct {
 type CreateProjectResponse Project
 
 type CreateProjectRoleRequest struct {
-	Kind      string `json:"kind" form:"required"`
-	UserID    uint   `json:"user_id" form:"required"`
-	ProjectID uint   `json:"project_id" form:"required"`
+	Kind   string `json:"kind" form:"required"`
+	UserID uint   `json:"user_id" form:"required"`
 }
 
-type CreateProjectRoleResponse ProjectRole
+type CreateProjectRoleResponse Role
 
 type ListProjectsRequest struct{}
 

+ 1 - 1
cmd/app/main.go

@@ -99,7 +99,7 @@ func main() {
 
 		errorChan := make(chan error)
 
-		go prov.GlobalStreamListener(redis, *repo, errorChan)
+		go prov.GlobalStreamListener(redis, repo, errorChan)
 	}
 
 	a, err := api.New(&api.AppConfig{

+ 17 - 8
internal/models/project.go

@@ -3,6 +3,7 @@ package models
 import (
 	"gorm.io/gorm"
 
+	"github.com/porter-dev/porter/api/types"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 )
 
@@ -57,16 +58,24 @@ func (p *Project) Externalize() *ProjectExternal {
 		roles = append(roles, *role.Externalize())
 	}
 
-	repos := make([]GitRepoExternal, 0)
+	return &ProjectExternal{
+		ID:    p.ID,
+		Name:  p.Name,
+		Roles: roles,
+	}
+}
 
-	for _, repo := range p.GitRepos {
-		repos = append(repos, *repo.Externalize())
+// ToProjectType generates an external types.Project to be shared over REST
+func (p *Project) ToProjectType() *types.Project {
+	roles := make([]*types.Role, 0)
+
+	for _, role := range p.Roles {
+		roles = append(roles, role.ToRoleType())
 	}
 
-	return &ProjectExternal{
-		ID:       p.ID,
-		Name:     p.Name,
-		Roles:    roles,
-		GitRepos: repos,
+	return &types.Project{
+		ID:    p.ID,
+		Name:  p.Name,
+		Roles: roles,
 	}
 }

+ 8 - 0
internal/models/role.go

@@ -28,3 +28,11 @@ func (r *Role) Externalize() *RoleExternal {
 		},
 	}
 }
+
+func (r *Role) ToRoleType() *types.Role {
+	return &types.Role{
+		Kind:      r.Kind,
+		UserID:    r.UserID,
+		ProjectID: r.ProjectID,
+	}
+}