satellite/console: add endpoint to get invite link

This change adds an endpoint that returns the invite link for invited
users.

Related to: #5762

Change-Id: I6432dfbe6405222b949fa02020aee2e01ab59c98
This commit is contained in:
Wilfred Asomani 2023-06-21 11:44:15 +00:00 committed by Storj Robot
parent ac1ff0e7e2
commit 2caa5052ad
5 changed files with 96 additions and 17 deletions

View File

@ -97,6 +97,38 @@ func (p *Projects) InviteUsers(w http.ResponseWriter, r *http.Request) {
}
}
// GetInviteLink returns a link to an invitation given project ID and invitee's email.
func (p *Projects) GetInviteLink(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
idParam, ok := mux.Vars(r)["id"]
if !ok {
p.serveJSONError(w, http.StatusBadRequest, errs.New("missing project id route param"))
return
}
id, err := uuid.FromString(idParam)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
}
email := r.URL.Query().Get("email")
if email == "" {
p.serveJSONError(w, http.StatusBadRequest, errs.New("missing email query param"))
return
}
link, err := p.service.GetInviteLink(ctx, id, email)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
}
err = json.NewEncoder(w).Encode(link)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
}
}
// GetUserInvitations returns the user's pending project member invitations.
func (p *Projects) GetUserInvitations(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

View File

@ -263,6 +263,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
projectsRouter := router.PathPrefix("/api/v0/projects").Subrouter()
projectsRouter.Handle("/{id}/salt", server.withAuth(http.HandlerFunc(projectsController.GetSalt))).Methods(http.MethodGet)
projectsRouter.Handle("/{id}/invite", server.withAuth(http.HandlerFunc(projectsController.InviteUsers))).Methods(http.MethodPost)
projectsRouter.Handle("/{id}/invite-link", server.withAuth(http.HandlerFunc(projectsController.GetInviteLink))).Methods(http.MethodGet)
projectsRouter.Handle("/invitations", server.withAuth(http.HandlerFunc(projectsController.GetUserInvitations))).Methods(http.MethodGet)
projectsRouter.Handle("/invitations/{id}/respond", server.withAuth(http.HandlerFunc(projectsController.RespondToInvitation))).Methods(http.MethodPost)

View File

@ -128,10 +128,10 @@ func TestInvitedRouting(t *testing.T) {
loginURL := baseURL + "login"
invalidURL := loginURL + "?invite_invalid=true"
tokenInvalidProj, err := service.CreateInviteToken(ctx1, project.ID, user2.Email, time.Now())
tokenInvalidProj, err := service.CreateInviteToken(ctx, project.ID, user2.Email, time.Now())
require.NoError(t, err)
token, err := service.CreateInviteToken(ctx1, project.PublicID, user2.Email, time.Now())
token, err := service.CreateInviteToken(ctx, project.PublicID, user2.Email, time.Now())
require.NoError(t, err)
checkInvitedRedirect("Invited - Invalid projectID", invalidURL, tokenInvalidProj)
@ -141,7 +141,7 @@ func TestInvitedRouting(t *testing.T) {
_, err = service.InviteProjectMembers(ctx1, project.ID, []string{user2.Email})
require.NoError(t, err)
token, err = service.CreateInviteToken(ctx1, project.PublicID, user2.Email, time.Now())
token, err = service.CreateInviteToken(ctx, project.PublicID, user2.Email, time.Now())
require.NoError(t, err)
// valid invite should redirect to login page with email.

View File

@ -3627,7 +3627,7 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID,
return err
}
}
token, err := s.CreateInviteToken(ctx, isMember.project.PublicID, invited.Email, invite.CreatedAt.Add(s.config.ProjectInvitationExpiration))
token, err := s.CreateInviteToken(ctx, isMember.project.PublicID, invited.Email, invite.CreatedAt)
if err != nil {
return err
}
@ -3698,20 +3698,41 @@ func (s *Service) GetInviteByToken(ctx context.Context, token string) (invite *P
return invite, nil
}
// CreateInviteToken creates a token for project invite links.
func (s *Service) CreateInviteToken(ctx context.Context, publicProjectID uuid.UUID, email string, inviteDate time.Time) (_ string, err error) {
// GetInviteLink returns a link for project invites.
func (s *Service) GetInviteLink(ctx context.Context, publicProjectID uuid.UUID, email string) (_ string, err error) {
defer mon.Task()(&ctx)(&err)
user, err := s.getUserAndAuditLog(ctx, "create invite token", zap.String("projectID", publicProjectID.String()), zap.String("email", email))
user, err := s.getUserAndAuditLog(ctx, "get invite link", zap.String("projectID", publicProjectID.String()), zap.String("email", email))
if err != nil {
return "", Error.Wrap(err)
}
_, err = s.isProjectMember(ctx, user.ID, publicProjectID)
isMember, err := s.isProjectMember(ctx, user.ID, publicProjectID)
if err != nil {
return "", Error.Wrap(err)
}
invite, err := s.store.ProjectInvitations().Get(ctx, isMember.project.ID, email)
if err != nil {
if !errs.Is(err, sql.ErrNoRows) {
return "", Error.Wrap(err)
}
return "", ErrProjectInviteInvalid.New(projInviteInvalidErrMsg)
}
token, err := s.CreateInviteToken(ctx, publicProjectID, email, invite.CreatedAt)
if err != nil {
return "", Error.Wrap(err)
}
return fmt.Sprintf("%s/invited?invite=%s", s.satelliteAddress, token), nil
}
// CreateInviteToken creates a token for project invite links.
// Internal use only, since it doesn't check if the project is valid or the user is a member of the project.
func (s *Service) CreateInviteToken(ctx context.Context, publicProjectID uuid.UUID, email string, inviteDate time.Time) (_ string, err error) {
defer mon.Task()(&ctx)(&err)
linkClaims := consoleauth.Claims{
ID: publicProjectID,
Email: email,

View File

@ -2074,19 +2074,10 @@ func TestProjectInvitations(t *testing.T) {
t.Run("invite tokens", func(t *testing.T) {
user, ctx1 := getUserAndCtx(t)
_, ctx2 := getUserAndCtx(t)
project, err := sat.AddProject(ctx1, user.ID, "Test Project")
require.NoError(t, err)
_, err = service.CreateInviteToken(ctx2, project.PublicID, email, time.Now())
require.Error(t, err)
require.True(t, console.ErrNoMembership.Has(err))
_, err = service.CreateInviteToken(ctx1, testrand.UUID(), email, time.Now())
require.Error(t, err)
require.ErrorIs(t, err, sql.ErrNoRows)
someToken, err := service.CreateInviteToken(ctx1, project.PublicID, email, time.Now())
require.NoError(t, err)
require.NotEmpty(t, someToken)
@ -2105,6 +2096,40 @@ func TestProjectInvitations(t *testing.T) {
require.True(t, console.ErrTokenExpiration.Has(err))
})
t.Run("invite links", func(t *testing.T) {
user, ctx1 := getUserAndCtx(t)
user2, ctx2 := getUserAndCtx(t)
project, err := sat.AddProject(ctx1, user.ID, "Test Project")
require.NoError(t, err)
_, err = service.GetInviteLink(ctx2, project.PublicID, user2.Email)
require.Error(t, err)
require.True(t, console.ErrNoMembership.Has(err))
// no such project
_, err = service.GetInviteLink(ctx1, testrand.UUID(), user2.Email)
require.Error(t, err)
require.ErrorIs(t, err, sql.ErrNoRows)
// no invite exists.
_, err = service.GetInviteLink(ctx1, project.PublicID, user2.Email)
require.Error(t, err)
require.True(t, console.ErrProjectInviteInvalid.Has(err))
invite := addInvite(t, ctx1, project, user2.Email)
someLink, err := service.GetInviteLink(ctx1, project.PublicID, user2.Email)
require.NoError(t, err)
require.NotEmpty(t, someLink)
someToken, err := service.CreateInviteToken(ctx1, project.PublicID, user2.Email, invite.CreatedAt)
require.NoError(t, err)
require.NotEmpty(t, someToken)
require.Contains(t, someLink, someToken)
})
t.Run("get invite by invite token", func(t *testing.T) {
owner, ctx := getUserAndCtx(t)
user, _ := getUserAndCtx(t)