Bladeren bron

basic project model and endpoints set up

Alexander Belanger 5 jaren geleden
bovenliggende
commit
7ead85598c

+ 1 - 1
cmd/app/main.go

@@ -54,7 +54,7 @@ func main() {
 
 	a := api.New(logger, repo, validator, store, appConf.Server.CookieName, false)
 
-	appRouter := router.New(a, store, appConf.Server.CookieName, appConf.Server.StaticFilePath)
+	appRouter := router.New(a, store, appConf.Server.CookieName, appConf.Server.StaticFilePath, repo)
 
 	address := fmt.Sprintf(":%d", appConf.Server.Port)
 

+ 43 - 0
internal/forms/project.go

@@ -0,0 +1,43 @@
+package forms
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// WriteProjectForm is a generic form for write operations to the Project model
+type WriteProjectForm interface {
+	ToProject(repo repository.ProjectRepository) (*models.Project, error)
+}
+
+// CreateProjectForm represents the accepted values for creating a project
+type CreateProjectForm struct {
+	WriteProjectForm
+	Name string `json:"name" form:"required"`
+}
+
+// ToProject converts the project to a gorm project model
+func (cpf *CreateProjectForm) ToProject(_ repository.ProjectRepository) (*models.Project, error) {
+	return &models.Project{
+		Name: cpf.Name,
+	}, nil
+}
+
+// CreateProjectRoleForm represents the accepted values for creating a project
+// role
+type CreateProjectRoleForm struct {
+	WriteProjectForm
+	ID    uint          `json:"project_id" form:"required"`
+	Roles []models.Role `json:"roles"`
+}
+
+// ToProject converts the form to a gorm project model
+func (cprf *CreateProjectRoleForm) ToProject(_ repository.ProjectRepository) (*models.Project, error) {
+	return &models.Project{
+		Model: gorm.Model{
+			ID: cprf.ID,
+		},
+		Roles: cprf.Roles,
+	}, nil
+}

+ 0 - 2
internal/kubernetes/local/kubeconfig.go

@@ -1,5 +1,3 @@
-// +build cli
-
 package local
 
 import (

+ 35 - 0
internal/models/project.go

@@ -0,0 +1,35 @@
+package models
+
+import (
+	"gorm.io/gorm"
+)
+
+// Project type that extends gorm.Model
+type Project struct {
+	gorm.Model
+
+	Name  string `json:"name"`
+	Roles []Role `json:"roles"`
+}
+
+// ProjectExternal represents the Project type that is sent over REST
+type ProjectExternal struct {
+	ID    uint           `json:"id"`
+	Name  string         `json:"name"`
+	Roles []RoleExternal `json:"roles"`
+}
+
+// Externalize generates an external Project to be shared over REST
+func (p *Project) Externalize() *ProjectExternal {
+	roles := make([]RoleExternal, 0)
+
+	for _, role := range p.Roles {
+		roles = append(roles, *role.Externalize())
+	}
+
+	return &ProjectExternal{
+		ID:    p.ID,
+		Name:  p.Name,
+		Roles: roles,
+	}
+}

+ 32 - 0
internal/models/role.go

@@ -0,0 +1,32 @@
+package models
+
+import (
+	"gorm.io/gorm"
+)
+
+// Role type that extends gorm.Model
+type Role struct {
+	gorm.Model
+
+	Kind      string `json:"kind"`
+	UserID    uint   `json:"user_id"`
+	ProjectID uint   `json:"project_id"`
+}
+
+// RoleExternal represents the Role type that is sent over REST
+type RoleExternal struct {
+	ID        uint   `json:"id"`
+	Kind      string `json:"kind"`
+	UserID    uint   `json:"user_id"`
+	ProjectID uint   `json:"project_id"`
+}
+
+// Externalize generates an external Role to be shared over REST
+func (r *Role) Externalize() *RoleExternal {
+	return &RoleExternal{
+		ID:        r.ID,
+		Kind:      r.Kind,
+		UserID:    r.UserID,
+		ProjectID: r.ProjectID,
+	}
+}

+ 0 - 2
internal/providers/gcp/local/config.go

@@ -1,5 +1,3 @@
-// +build cli
-
 package local
 
 import (

+ 35 - 0
internal/repository/gorm/project.go

@@ -0,0 +1,35 @@
+package gorm
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// ProjectRepository uses gorm.DB for querying the database
+type ProjectRepository struct {
+	db *gorm.DB
+}
+
+// NewProjectRepository returns a ProjectRepository which uses
+// gorm.DB for querying the database
+func NewProjectRepository(db *gorm.DB) repository.ProjectRepository {
+	return &ProjectRepository{db}
+}
+
+// CreateProject creates a new project
+func (repo *ProjectRepository) CreateProject(project *models.Project) (*models.Project, error) {
+	return nil, errors.New("UNIMPLEMENTED")
+}
+
+// CreateProjectRole appends a role to the existing array of roles
+func (repo *ProjectRepository) CreateProjectRole(project *models.Project, role *models.Role) (*models.Role, error) {
+	return nil, errors.New("UNIMPLEMENTED")
+}
+
+// ReadProject gets a projects specified by a unique id
+func (repo *ProjectRepository) ReadProject(id uint) (*models.Project, error) {
+	return nil, errors.New("UNIMPLEMENTED")
+}

+ 1 - 0
internal/repository/gorm/repository.go

@@ -11,5 +11,6 @@ func NewRepository(db *gorm.DB) *repository.Repository {
 	return &repository.Repository{
 		User:    NewUserRepository(db),
 		Session: NewSessionRepository(db),
+		Project: NewProjectRepository(db),
 	}
 }

+ 15 - 0
internal/repository/project.go

@@ -0,0 +1,15 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// WriteProject is the function type for all Project write operations
+type WriteProject func(project *models.Project) (*models.Project, error)
+
+// ProjectRepository represents the set of queries on the Project model
+type ProjectRepository interface {
+	CreateProject(project *models.Project) (*models.Project, error)
+	CreateProjectRole(project *models.Project, role *models.Role) (*models.Role, error)
+	ReadProject(id uint) (*models.Project, error)
+}

+ 1 - 0
internal/repository/repository.go

@@ -3,5 +3,6 @@ package repository
 // Repository collects the repositories for each model
 type Repository struct {
 	User    UserRepository
+	Project ProjectRepository
 	Session SessionRepository
 }

+ 66 - 0
internal/repository/test/project.go

@@ -0,0 +1,66 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// ProjectRepository will return errors on queries if canQuery is false
+// and only stores a small set of projects in-memory that are indexed by their
+// array index + 1
+type ProjectRepository struct {
+	canQuery bool
+	projects []*models.Project
+}
+
+// NewProjectRepository will return errors if canQuery is false
+func NewProjectRepository(canQuery bool) repository.ProjectRepository {
+	return &ProjectRepository{canQuery, []*models.Project{}}
+}
+
+// CreateProject appends a new project to the in-memory projects array
+func (repo *ProjectRepository) CreateProject(project *models.Project) (*models.Project, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.projects = append(repo.projects, project)
+	project.ID = uint(len(repo.projects))
+
+	return project, nil
+}
+
+// CreateProjectRole appends a role to the existing array of roles
+func (repo *ProjectRepository) CreateProjectRole(project *models.Project, role *models.Role) (*models.Role, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(project.ID-1) >= len(repo.projects) || repo.projects[project.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(project.ID - 1)
+	oldProject := *repo.projects[index]
+	repo.projects[index] = project
+	project.Roles = append(oldProject.Roles, *role)
+
+	return role, nil
+}
+
+// ReadProject gets a projects specified by a unique id
+func (repo *ProjectRepository) ReadProject(id uint) (*models.Project, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.projects) || repo.projects[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.projects[index], nil
+}

+ 1 - 0
internal/repository/test/repository.go

@@ -10,5 +10,6 @@ func NewRepository(canQuery bool) *repository.Repository {
 	return &repository.Repository{
 		User:    NewUserRepository(canQuery),
 		Session: NewSessionRepository(canQuery),
+		Project: NewProjectRepository(canQuery),
 	}
 }

+ 1 - 1
server/api/helpers_test.go

@@ -80,7 +80,7 @@ func newTester(canQuery bool) *tester {
 
 	store, _ := sessionstore.NewStore(repo, appConf.Server)
 	app := api.New(logger, repo, validator, store, appConf.Server.CookieName, true)
-	r := router.New(app, store, appConf.Server.CookieName, appConf.Server.StaticFilePath)
+	r := router.New(app, store, appConf.Server.CookieName, appConf.Server.StaticFilePath, repo)
 
 	return &tester{
 		app:    app,

+ 104 - 0
server/api/project_handler.go

@@ -0,0 +1,104 @@
+package api
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// Enumeration of user API error codes, represented as int64
+const (
+	ErrProjectDecode ErrorCode = iota + 600
+	ErrProjectValidateFields
+	ErrProjectDataRead
+)
+
+// HandleCreateProject validates a project form entry, converts the project to a gorm
+// model, and saves the user to the database
+func (app *App) HandleCreateProject(w http.ResponseWriter, r *http.Request) {
+	session, err := app.store.Get(r, app.cookieName)
+
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+
+	form := &forms.CreateProjectForm{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to a project model
+	projModel, err := form.ToProject(app.repo.Project)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	projModel, err = app.repo.Project.CreateProject(projModel)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	// create a new Role with the user as the owner
+	_, err = app.repo.Project.CreateProjectRole(projModel, &models.Role{
+		UserID:    userID,
+		ProjectID: projModel.ID,
+		Kind:      "Owner",
+	})
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	app.logger.Info().Msgf("New project created: %d", projModel.ID)
+
+	w.WriteHeader(http.StatusCreated)
+}
+
+// HandleReadProject returns an externalized Project (models.ProjectExternal)
+// based on an ID
+func (app *App) HandleReadProject(w http.ResponseWriter, r *http.Request) {
+	id, err := strconv.ParseUint(chi.URLParam(r, "id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	proj, err := app.repo.Project.ReadProject(uint(id))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	projExt := proj.Externalize()
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(projExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+}

+ 154 - 0
server/api/project_handler_test.go

@@ -0,0 +1,154 @@
+package api_test
+
+import (
+	"encoding/json"
+	"net/http"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
+
+type projTest struct {
+	initializers []func(t *tester)
+	msg          string
+	method       string
+	endpoint     string
+	body         string
+	expStatus    int
+	expBody      string
+	useCookie    bool
+	validators   []func(c *projTest, tester *tester, t *testing.T)
+}
+
+func testProjRequests(t *testing.T, tests []*projTest, canQuery bool) {
+	for _, c := range tests {
+		// create a new tester
+		tester := newTester(canQuery)
+
+		// if there's an initializer, call it
+		for _, init := range c.initializers {
+			init(tester)
+		}
+
+		req, err := http.NewRequest(
+			c.method,
+			c.endpoint,
+			strings.NewReader(c.body),
+		)
+
+		tester.req = req
+
+		if c.useCookie {
+			req.AddCookie(tester.cookie)
+		}
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		tester.execute()
+		rr := tester.rr
+
+		// first, check that the status matches
+		if status := rr.Code; status != c.expStatus {
+			t.Errorf("%s, handler returned wrong status code: got %v want %v",
+				c.msg, status, c.expStatus)
+		}
+
+		// if there's a validator, call it
+		for _, validate := range c.validators {
+			validate(c, tester, t)
+		}
+	}
+}
+
+// ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
+
+var createProjectTests = []*projTest{
+	&projTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+		},
+		msg:      "Create project",
+		method:   "POST",
+		endpoint: "/api/projects",
+		body: `{
+			"name": "project-test"
+		}`,
+		expStatus: http.StatusCreated,
+		expBody:   ``,
+		useCookie: true,
+		validators: []func(c *projTest, tester *tester, t *testing.T){
+			projectBasicBodyValidator,
+		},
+	},
+}
+
+func TestHandleCreateProject(t *testing.T) {
+	testProjRequests(t, createProjectTests, true)
+}
+
+var readProjectTests = []*projTest{
+	&projTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+		},
+		msg:       "Read project",
+		method:    "GET",
+		endpoint:  "/api/projects/1",
+		body:      ``,
+		expStatus: http.StatusOK,
+		expBody:   `{"id":1,"name":"project-test","roles":[{"id":0,"kind":"Owner","user_id":1,"project_id":1}]}`,
+		useCookie: true,
+		validators: []func(c *projTest, tester *tester, t *testing.T){
+			projectModelBodyValidator,
+		},
+	},
+}
+
+func TestHandleReadProject(t *testing.T) {
+	testProjRequests(t, readProjectTests, true)
+}
+
+// ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
+
+func initProject(tester *tester) {
+	user, _ := tester.repo.User.ReadUserByEmail("belanger@getporter.dev")
+
+	// handle write to the database
+	projModel, _ := tester.repo.Project.CreateProject(&models.Project{
+		Name: "project-test",
+	})
+
+	// create a new Role with the user as the owner
+	tester.repo.Project.CreateProjectRole(projModel, &models.Role{
+		UserID:    user.ID,
+		ProjectID: projModel.ID,
+		Kind:      "Owner",
+	})
+}
+
+func projectBasicBodyValidator(c *projTest, tester *tester, t *testing.T) {
+	if body := tester.rr.Body.String(); strings.TrimSpace(body) != strings.TrimSpace(c.expBody) {
+		t.Errorf("%s, handler returned wrong body: got %v want %v",
+			c.msg, body, c.expBody)
+	}
+}
+
+func projectModelBodyValidator(c *projTest, tester *tester, t *testing.T) {
+	gotBody := &models.ProjectExternal{}
+	expBody := &models.ProjectExternal{}
+
+	json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
+	json.Unmarshal([]byte(c.expBody), expBody)
+
+	if !reflect.DeepEqual(gotBody, expBody) {
+		t.Errorf("%s, handler returned wrong body: got %v want %v",
+			c.msg, gotBody, expBody)
+	}
+}

+ 7 - 4
server/api/user_handler_test.go

@@ -80,7 +80,7 @@ var authCheckTests = []*userTest{
 		endpoint:  "/api/auth/check",
 		expStatus: http.StatusOK,
 		body:      "",
-		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
+		expBody:   `{"id":1,"email":"belanger@getporter.dev","contexts":null,"rawKubeConfig":""}`,
 		useCookie: true,
 		validators: []func(c *userTest, tester *tester, t *testing.T){
 			userBasicBodyValidator,
@@ -116,7 +116,10 @@ var createUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusCreated,
-		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
+		expBody:   `{"id":1,"email":"belanger@getporter.dev","contexts":null,"rawKubeConfig":""}`,
+		validators: []func(c *userTest, tester *tester, t *testing.T){
+			userModelBodyValidator,
+		},
 	},
 	&userTest{
 		msg:      "Create user invalid email",
@@ -216,7 +219,7 @@ var loginUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusOK,
-		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
+		expBody:   `{"id":1,"email":"belanger@getporter.dev","contexts":null,"rawKubeConfig":""}`,
 		validators: []func(c *userTest, tester *tester, t *testing.T){
 			userBasicBodyValidator,
 		},
@@ -233,7 +236,7 @@ var loginUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusOK,
-		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
+		expBody:   `{"id":1,"email":"belanger@getporter.dev","contexts":null,"rawKubeConfig":""}`,
 		useCookie: true,
 		validators: []func(c *userTest, tester *tester, t *testing.T){
 			userBasicBodyValidator,

+ 93 - 15
server/router/middleware/auth.go

@@ -10,20 +10,23 @@ import (
 
 	"github.com/go-chi/chi"
 	"github.com/gorilla/sessions"
+	"github.com/porter-dev/porter/internal/repository"
 )
 
 // Auth implements the authorization functions
 type Auth struct {
 	store      sessions.Store
 	cookieName string
+	repo       *repository.Repository
 }
 
 // NewAuth returns a new Auth instance
 func NewAuth(
 	store sessions.Store,
 	cookieName string,
+	repo *repository.Repository,
 ) *Auth {
-	return &Auth{store, cookieName}
+	return &Auth{store, cookieName, repo}
 }
 
 // BasicAuthenticate just checks that a user is logged in
@@ -50,37 +53,74 @@ const (
 	BodyParam
 )
 
-type bodyID struct {
+type bodyUserID struct {
 	UserID uint64 `json:"user_id"`
 }
 
+type bodyProjectID struct {
+	ProjectID uint64 `json:"project_id"`
+}
+
 // DoesUserIDMatch checks the id URL parameter and verifies that it matches
 // the one stored in the session
 func (auth *Auth) DoesUserIDMatch(next http.Handler, loc IDLocation) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		var id uint64
 		var err error
+		id := findUserIDInRequest(r, loc)
 
-		if loc == URLParam {
-			id, err = strconv.ParseUint(chi.URLParam(r, "id"), 0, 64)
-		} else if loc == BodyParam {
-			form := &bodyID{}
-			body, _ := ioutil.ReadAll(r.Body)
-			err = json.Unmarshal(body, form)
+		if err == nil && auth.doesSessionMatchID(r, uint(id)) {
+			next.ServeHTTP(w, r)
+		} else {
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
+		}
 
-			id = form.UserID
+		return
+	})
+}
 
-			// need to create a new stream for the body
-			r.Body = ioutil.NopCloser(bytes.NewReader(body))
+// DoesUserHaveProjectReadAccess looks for a project_id parameter and checks that the
+// user has access to read that project
+func (auth *Auth) DoesUserHaveProjectReadAccess(
+	next http.Handler,
+	userLoc IDLocation,
+	projLoc IDLocation,
+) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var err error
+		projID := uint(findProjIDInRequest(r, projLoc))
+
+		session, err := auth.store.Get(r, auth.cookieName)
+
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
 		}
 
-		if err == nil && auth.doesSessionMatchID(r, uint(id)) {
-			next.ServeHTTP(w, r)
-		} else {
+		userID, ok := session.Values["user_id"].(uint)
+
+		if !ok {
 			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
 			return
 		}
 
+		// get the project
+		proj, err := auth.repo.Project.ReadProject(projID)
+
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+			return
+		}
+
+		// look for the user role in the project
+		for _, role := range proj.Roles {
+			if role.UserID == userID {
+				next.ServeHTTP(w, r)
+				return
+			}
+		}
+
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
 		return
 	})
 }
@@ -111,3 +151,41 @@ func (auth *Auth) isLoggedIn(w http.ResponseWriter, r *http.Request) bool {
 	}
 	return true
 }
+
+func findUserIDInRequest(r *http.Request, userLoc IDLocation) uint64 {
+	var userID uint64
+
+	if userLoc == URLParam {
+		userID, _ = strconv.ParseUint(chi.URLParam(r, "id"), 0, 64)
+	} else if userLoc == BodyParam {
+		form := &bodyUserID{}
+		body, _ := ioutil.ReadAll(r.Body)
+		_ = json.Unmarshal(body, form)
+
+		userID = form.UserID
+
+		// need to create a new stream for the body
+		r.Body = ioutil.NopCloser(bytes.NewReader(body))
+	}
+
+	return userID
+}
+
+func findProjIDInRequest(r *http.Request, projLoc IDLocation) uint64 {
+	var projID uint64
+
+	if projLoc == URLParam {
+		projID, _ = strconv.ParseUint(chi.URLParam(r, "id"), 0, 64)
+	} else if projLoc == BodyParam {
+		form := &bodyProjectID{}
+		body, _ := ioutil.ReadAll(r.Body)
+		_ = json.Unmarshal(body, form)
+
+		projID = form.ProjectID
+
+		// need to create a new stream for the body
+		r.Body = ioutil.NopCloser(bytes.NewReader(body))
+	}
+
+	return projID
+}

+ 7 - 1
server/router/router.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/go-chi/chi"
 	"github.com/gorilla/sessions"
+	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/server/api"
 	"github.com/porter-dev/porter/server/requestlog"
 	mw "github.com/porter-dev/porter/server/router/middleware"
@@ -17,10 +18,11 @@ func New(
 	store sessions.Store,
 	cookieName string,
 	staticFilePath string,
+	repo *repository.Repository,
 ) *chi.Mux {
 	l := a.Logger()
 	r := chi.NewRouter()
-	auth := mw.NewAuth(store, cookieName)
+	auth := mw.NewAuth(store, cookieName, repo)
 
 	r.Route("/api", func(r chi.Router) {
 		r.Use(mw.ContentTypeJSON)
@@ -35,6 +37,10 @@ func New(
 		r.Method("GET", "/auth/check", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleAuthCheck, l)))
 		r.Method("POST", "/logout", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleLogoutUser, l)))
 
+		// /api/projects routes
+		r.Method("GET", "/projects/{id}", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleReadProject, l)))
+		r.Method("POST", "/projects", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleCreateProject, l)))
+
 		// /api/releases routes
 		r.Method("GET", "/releases", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleListReleases, l)))
 		r.Method("GET", "/releases/{name}/{revision}/components", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleGetReleaseComponents, l)))