Browse Source

add invite endpoints

Anukul Sangwan 4 năm trước cách đây
mục cha
commit
2ce57f57ae

+ 2 - 0
api/server/authz/policy.go

@@ -110,6 +110,8 @@ func getRequestActionForEndpoint(
 			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamNamespace)
 		case types.ReleaseScope:
 			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamReleaseName)
+		case types.InviteScope:
+			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamInviteID)
 		}
 
 		if reqErr != nil {

+ 90 - 0
api/server/handlers/invite/create.go

@@ -0,0 +1,90 @@
+package invite
+
+import (
+	"fmt"
+	"net/http"
+	"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/server/shared/config"
+	"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/oauth"
+)
+
+type CreateInviteHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCreateInviteHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateInviteHandler {
+	return &CreateInviteHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *CreateInviteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.CreateInviteRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	// create invite model
+	invite, err := CreateInviteWithProject(request, project.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// write to database
+	invite, err = c.Repo().Invite().CreateInvite(invite)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// app.Logger.Info().Msgf("New invite created: %d", invite.ID)
+
+	if err := c.Config().UserNotifier.SendProjectInviteEmail(
+		&notifier.SendProjectInviteEmailOpts{
+			InviteeEmail:      request.Email,
+			URL:               fmt.Sprintf("%s/api/projects/%d/invites/%s", c.Config().ServerConf.ServerURL, project.ID, invite.Token),
+			Project:           project.Name,
+			ProjectOwnerEmail: user.Email,
+		},
+	); err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := types.CreateInviteResponse{
+		Invite: invite.ToInviteType(),
+	}
+
+	c.WriteResult(w, r, res)
+}
+
+func CreateInviteWithProject(invite *types.CreateInviteRequest, projectID uint) (*models.Invite, error) {
+	// generate a token and an expiry time
+	expiry := time.Now().Add(24 * time.Hour)
+
+	return &models.Invite{
+		Email:     invite.Email,
+		Kind:      invite.Kind,
+		Expiry:    &expiry,
+		ProjectID: projectID,
+		Token:     oauth.CreateRandomState(),
+	}, nil
+}

+ 36 - 0
api/server/handlers/invite/delete.go

@@ -0,0 +1,36 @@
+package invite
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type InviteDeleteHandler struct {
+	handlers.PorterHandler
+	authz.KubernetesAgentGetter
+}
+
+func NewInviteDeleteHandler(
+	config *config.Config,
+) *InviteDeleteHandler {
+	return &InviteDeleteHandler{
+		PorterHandler:         handlers.NewDefaultPorterHandler(config, nil, nil),
+		KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *InviteDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	invite, _ := r.Context().Value(types.InviteScope).(*models.Invite)
+
+	if err := c.Repo().Invite().DeleteInvite(invite); err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	}
+
+	w.WriteHeader(http.StatusOK)
+}

+ 44 - 0
api/server/handlers/invite/list.go

@@ -0,0 +1,44 @@
+package invite
+
+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/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ListInvitesHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewListInvitesHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListInvitesHandler {
+	return &ListInvitesHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *ListInvitesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	invites, err := c.Repo().Invite().ListInvitesByProjectID(project.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.ListInvitesResponse = make([]*types.Invite, 0)
+
+	for _, invite := range invites {
+		res = append(res, invite.ToInviteType())
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 43 - 0
api/server/handlers/invite/update_role.go

@@ -0,0 +1,43 @@
+package invite
+
+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/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type InviteUpdateRoleHandler struct {
+	handlers.PorterHandlerReader
+}
+
+func NewInviteUpdateRoleHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+) *InviteUpdateRoleHandler {
+	return &InviteUpdateRoleHandler{
+		PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, nil),
+	}
+}
+
+func (c *InviteUpdateRoleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	invite, _ := r.Context().Value(types.InviteScope).(*models.Invite)
+
+	request := &types.UpdateInviteRoleRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	invite.Kind = request.Kind
+
+	if _, err := c.Repo().Invite().UpdateInvite(invite); err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	}
+
+	w.WriteHeader(http.StatusOK)
+}

+ 66 - 0
api/server/handlers/project/list_collaborators.go

@@ -0,0 +1,66 @@
+package project
+
+import (
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ProjectListCollaboratorsHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewProjectListCollaboratorsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ProjectListCollaboratorsHandler {
+	return &ProjectListCollaboratorsHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ProjectListCollaboratorsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	roles, err := p.Repo().Project().ListProjectRoles(proj.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	roleMap := make(map[uint]*models.Role)
+	idArr := make([]uint, 0)
+
+	for _, role := range roles {
+		roleCp := role
+		roleMap[role.UserID] = &roleCp
+		idArr = append(idArr, role.UserID)
+	}
+
+	users, err := p.Repo().User().ListUsersByIDs(idArr)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.ListCollaboratorsResponse = make([]*types.Collaborator, 0)
+
+	for _, user := range users {
+		res = append(res, &types.Collaborator{
+			ID:        roleMap[user.ID].ID,
+			Kind:      string(roleMap[user.ID].Kind),
+			UserID:    roleMap[user.ID].UserID,
+			Email:     user.Email,
+			ProjectID: roleMap[user.ID].ProjectID,
+		})
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 30 - 0
api/server/handlers/project/list_roles.go

@@ -0,0 +1,30 @@
+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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ProjectListRolesHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewProjectListRolesHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ProjectListRolesHandler {
+	return &ProjectListRolesHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ProjectListRolesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	var res types.ListProjectRolesResponse = []string{models.RoleAdmin, models.RoleDeveloper, models.RoleViewer}
+
+	p.WriteResult(w, r, res)
+}

+ 55 - 0
api/server/router/invite.go

@@ -80,5 +80,60 @@ func getInviteRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/invites/{invite_id} -> invite.NewInviteUpdateRoleHandler
+	updateRoleEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InviteScope,
+			},
+		},
+	)
+
+	updateRoleHandler := invite.NewInviteUpdateRoleHandler(
+		config,
+		factory.GetDecoderValidator(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: updateRoleEndpoint,
+		Handler:  updateRoleHandler,
+		Router:   r,
+	})
+
+	// DELETE /api/projects/{project_id}/invites/{invite_id} -> invite.NewInviteGetHandler
+	deleteEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InviteScope,
+			},
+		},
+	)
+
+	deleteHandler := invite.NewInviteDeleteHandler(
+		config,
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: deleteEndpoint,
+		Handler:  deleteHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

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

@@ -4,6 +4,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/cluster"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
+	"github.com/porter-dev/porter/api/server/handlers/invite"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/handlers/registry"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -190,6 +191,60 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/collaborators -> project.NewProjectListCollaboratorsHandler
+	listCollaboratorsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/collaborators",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listCollaboratorsHandler := project.NewProjectListCollaboratorsHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listCollaboratorsEndpoint,
+		Handler:  listCollaboratorsHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/roles -> project.NewProjectListRolesHandler
+	listRolesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/roles",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listRolesHandler := project.NewProjectListRolesHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listRolesEndpoint,
+		Handler:  listRolesHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/registries -> registry.NewRegistryListHandler
 	listRegistriesEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
@@ -357,5 +412,60 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/invites -> invite.NewCreateInviteHandler
+	listInvitesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/invites",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listInvitesHandler := invite.NewListInvitesHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listInvitesEndpoint,
+		Handler:  listInvitesHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/invites -> invite.NewCreateInviteHandler
+	createInviteEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/invites",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	createInviteHandler := invite.NewCreateInviteHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: createInviteEndpoint,
+		Handler:  createInviteHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 15 - 0
api/types/invite.go

@@ -10,3 +10,18 @@ type Invite struct {
 }
 
 type GetInviteResponse Invite
+
+type CreateInviteRequest struct {
+	Email string `json:"email,required"`
+	Kind  string `json:"kind,required"`
+}
+
+type CreateInviteResponse struct {
+	*Invite
+}
+
+type ListInvitesResponse []*Invite
+
+type UpdateInviteRoleRequest struct {
+	Kind string `json:"kind,required"`
+}

+ 12 - 0
api/types/project.go

@@ -32,3 +32,15 @@ type DeleteProjectResponse Project
 type ListProjectInfraResponse []*Infra
 
 type GetProjectPolicyResponse []*PolicyDocument
+
+type ListProjectRolesResponse []string
+
+type Collaborator struct {
+	ID        uint   `json:"id"`
+	Kind      string `json:"kind"`
+	UserID    uint   `json:"user_id"`
+	Email     string `json:"email"`
+	ProjectID uint   `json:"project_id"`
+}
+
+type ListCollaboratorsResponse []*Collaborator

+ 1 - 1
go.mod

@@ -6,7 +6,7 @@ require (
 	cloud.google.com/go v0.65.0
 	github.com/AlecAivazis/survey/v2 v2.2.9
 	github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect
-	github.com/Masterminds/semver v1.5.0
+	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Masterminds/semver/v3 v3.1.1
 	github.com/aws/aws-sdk-go v1.35.4
 	github.com/bradleyfalzon/ghinstallation v1.1.1

+ 2 - 1
internal/models/invite.go

@@ -3,8 +3,9 @@ package models
 import (
 	"time"
 
-	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
+
+	"github.com/porter-dev/porter/api/types"
 )
 
 // Invite type that extends gorm.Model