From 28b2384970b0ad0429c189948ead243542a980aa Mon Sep 17 00:00:00 2001 From: Jeremy Wharton Date: Tue, 20 Jun 2023 22:25:19 -0500 Subject: [PATCH] satellite/console/.../consoleql: add project invite expiration status Each project member invitation returned from our GraphQL API now contains a field indicating whether the invitation has expired. This is required for us to enable functionality in the satellite frontend that is dependent on this information. References #5752 Change-Id: I4b71738e7a7373c690de188614f8c95009bc3989 --- satellite/console/consoleweb/consoleql/project.go | 11 ++++++++++- .../console/consoleweb/consoleql/projectmember.go | 14 +++++++++++++- satellite/console/service.go | 13 +++++++++---- satellite/console/service_test.go | 12 +++++++++--- web/satellite/src/api/projectMembers.ts | 4 +++- web/satellite/src/types/projectMembers.ts | 1 + 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/satellite/console/consoleweb/consoleql/project.go b/satellite/console/consoleweb/consoleql/project.go index 96e24de4b..32ca6eb56 100644 --- a/satellite/console/consoleweb/consoleql/project.go +++ b/satellite/console/consoleweb/consoleql/project.go @@ -166,9 +166,18 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec }) } + var invites []projectInvitation + for _, invite := range page.ProjectInvitations { + invites = append(invites, projectInvitation{ + Email: invite.Email, + CreatedAt: invite.CreatedAt, + Expired: service.IsProjectInvitationExpired(&invite), + }) + } + projectMembersPage := projectMembersPage{ ProjectMembers: users, - ProjectInvitations: page.ProjectInvitations, + ProjectInvitations: invites, TotalCount: page.TotalCount, Offset: page.Offset, Limit: page.Limit, diff --git a/satellite/console/consoleweb/consoleql/projectmember.go b/satellite/console/consoleweb/consoleql/projectmember.go index d0e8a5fe0..da9599faf 100644 --- a/satellite/console/consoleweb/consoleql/projectmember.go +++ b/satellite/console/consoleweb/consoleql/projectmember.go @@ -18,6 +18,8 @@ const ( ProjectInvitationType = "projectInvitation" // FieldJoinedAt is a field name for joined at timestamp. FieldJoinedAt = "joinedAt" + // FieldExpired is a field name for expiration status. + FieldExpired = "expired" ) // graphqlProjectMember creates projectMember type. @@ -51,6 +53,9 @@ func graphqlProjectInvitation() *graphql.Object { FieldCreatedAt: &graphql.Field{ Type: graphql.DateTime, }, + FieldExpired: &graphql.Field{ + Type: graphql.Boolean, + }, }, }) } @@ -122,9 +127,16 @@ type projectMember struct { JoinedAt time.Time } +// projectInvitation encapsulates a console.ProjectInvitation and its expiration status. +type projectInvitation struct { + Email string + CreatedAt time.Time + Expired bool +} + type projectMembersPage struct { ProjectMembers []projectMember - ProjectInvitations []console.ProjectInvitation + ProjectInvitations []projectInvitation Search string Limit uint diff --git a/satellite/console/service.go b/satellite/console/service.go index c7cade282..34d0a28d0 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -3471,7 +3471,7 @@ func (s *Service) GetUserProjectInvitations(ctx context.Context) (_ []ProjectInv var active []ProjectInvitation for _, invite := range invites { - if !time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) { + if !s.IsProjectInvitationExpired(&invite) { active = append(active, invite) } } @@ -3544,7 +3544,7 @@ func (s *Service) RespondToProjectInvitation(ctx context.Context, projectID uuid return ErrProjectInviteInvalid.New(projInviteInvalidErrMsg) } - if time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) { + if s.IsProjectInvitationExpired(invite) { deleteWithLog() return ErrProjectInviteInvalid.New(projInviteInvalidErrMsg) } @@ -3596,7 +3596,7 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID, if err != nil && !errs.Is(err, sql.ErrNoRows) { return nil, Error.Wrap(err) } - if invite != nil && !time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) { + if invite != nil && !s.IsProjectInvitationExpired(invite) { return nil, ErrProjectInviteActive.New(projInviteActiveErrMsg, invitedUser.Email) } users = append(users, invitedUser) @@ -3662,6 +3662,11 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID, return invites, nil } +// IsProjectInvitationExpired returns whether the project member invitation has expired. +func (s *Service) IsProjectInvitationExpired(invite *ProjectInvitation) bool { + return time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) +} + // GetInviteByToken returns a project invite given an invite token. func (s *Service) GetInviteByToken(ctx context.Context, token string) (invite *ProjectInvitation, err error) { defer mon.Task()(&ctx)(&err) @@ -3686,7 +3691,7 @@ func (s *Service) GetInviteByToken(ctx context.Context, token string) (invite *P } return nil, ErrProjectInviteInvalid.New(projInviteInvalidErrMsg) } - if time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) { + if s.IsProjectInvitationExpired(invite) { return nil, ErrProjectInviteInvalid.New(projInviteInvalidErrMsg) } diff --git a/satellite/console/service_test.go b/satellite/console/service_test.go index c93fbea1d..d2b1a3b85 100644 --- a/satellite/console/service_test.go +++ b/satellite/console/service_test.go @@ -1987,10 +1987,11 @@ func TestProjectInvitations(t *testing.T) { expireInvite := func(t *testing.T, ctx context.Context, invite *console.ProjectInvitation) { createdAt := time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration) - _, err := sat.DB.Console().ProjectInvitations().Update(ctx, invite.ProjectID, invite.Email, console.UpdateProjectInvitationRequest{ + newInvite, err := sat.DB.Console().ProjectInvitations().Update(ctx, invite.ProjectID, invite.Email, console.UpdateProjectInvitationRequest{ CreatedAt: &createdAt, }) require.NoError(t, err) + *invite = *newInvite } t.Run("invite users", func(t *testing.T) { @@ -2031,13 +2032,18 @@ func TestProjectInvitations(t *testing.T) { require.True(t, console.ErrProjectInviteActive.Has(err)) require.Empty(t, invites) - // resending an expired invitation should succeed. + // expire the invitation. + require.False(t, service.IsProjectInvitationExpired(&user3Invite)) + oldCreatedAt := user3Invite.CreatedAt expireInvite(t, ctx, &user3Invite) + require.True(t, service.IsProjectInvitationExpired(&user3Invite)) + + // resending an expired invitation should succeed. invites, err = service.InviteProjectMembers(ctx2, project.ID, []string{user3.Email}) require.NoError(t, err) require.Len(t, invites, 1) require.Equal(t, user2.ID, *invites[0].InviterID) - require.True(t, invites[0].CreatedAt.After(user3Invite.CreatedAt)) + require.True(t, invites[0].CreatedAt.After(oldCreatedAt)) // inviting a project member should fail. _, err = service.InviteProjectMembers(ctx, project.ID, []string{user2.Email}) diff --git a/web/satellite/src/api/projectMembers.ts b/web/satellite/src/api/projectMembers.ts index dc003ddc2..491dfa692 100644 --- a/web/satellite/src/api/projectMembers.ts +++ b/web/satellite/src/api/projectMembers.ts @@ -64,7 +64,8 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi { }, projectInvitations { email, - createdAt + createdAt, + expired }, search, limit, @@ -127,6 +128,7 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi { projectMembersPage.projectInvitations = projectMembers.projectInvitations.map(key => new ProjectInvitationItemModel( key.email, new Date(key.createdAt), + key.expired, )); projectMembersPage.search = projectMembers.search; diff --git a/web/satellite/src/types/projectMembers.ts b/web/satellite/src/types/projectMembers.ts index ed5228d16..ffe2b61be 100644 --- a/web/satellite/src/types/projectMembers.ts +++ b/web/satellite/src/types/projectMembers.ts @@ -231,6 +231,7 @@ export class ProjectInvitationItemModel implements ProjectMemberItemModel { public constructor( public email: string, public createdAt: Date, + public expired: boolean, ) {} /**