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
This commit is contained in:
Jeremy Wharton 2023-06-20 22:25:19 -05:00
parent d18f4f7d99
commit 28b2384970
6 changed files with 45 additions and 10 deletions

View File

@ -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{ projectMembersPage := projectMembersPage{
ProjectMembers: users, ProjectMembers: users,
ProjectInvitations: page.ProjectInvitations, ProjectInvitations: invites,
TotalCount: page.TotalCount, TotalCount: page.TotalCount,
Offset: page.Offset, Offset: page.Offset,
Limit: page.Limit, Limit: page.Limit,

View File

@ -18,6 +18,8 @@ const (
ProjectInvitationType = "projectInvitation" ProjectInvitationType = "projectInvitation"
// FieldJoinedAt is a field name for joined at timestamp. // FieldJoinedAt is a field name for joined at timestamp.
FieldJoinedAt = "joinedAt" FieldJoinedAt = "joinedAt"
// FieldExpired is a field name for expiration status.
FieldExpired = "expired"
) )
// graphqlProjectMember creates projectMember type. // graphqlProjectMember creates projectMember type.
@ -51,6 +53,9 @@ func graphqlProjectInvitation() *graphql.Object {
FieldCreatedAt: &graphql.Field{ FieldCreatedAt: &graphql.Field{
Type: graphql.DateTime, Type: graphql.DateTime,
}, },
FieldExpired: &graphql.Field{
Type: graphql.Boolean,
},
}, },
}) })
} }
@ -122,9 +127,16 @@ type projectMember struct {
JoinedAt time.Time 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 { type projectMembersPage struct {
ProjectMembers []projectMember ProjectMembers []projectMember
ProjectInvitations []console.ProjectInvitation ProjectInvitations []projectInvitation
Search string Search string
Limit uint Limit uint

View File

@ -3471,7 +3471,7 @@ func (s *Service) GetUserProjectInvitations(ctx context.Context) (_ []ProjectInv
var active []ProjectInvitation var active []ProjectInvitation
for _, invite := range invites { for _, invite := range invites {
if !time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) { if !s.IsProjectInvitationExpired(&invite) {
active = append(active, invite) active = append(active, invite)
} }
} }
@ -3544,7 +3544,7 @@ func (s *Service) RespondToProjectInvitation(ctx context.Context, projectID uuid
return ErrProjectInviteInvalid.New(projInviteInvalidErrMsg) return ErrProjectInviteInvalid.New(projInviteInvalidErrMsg)
} }
if time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) { if s.IsProjectInvitationExpired(invite) {
deleteWithLog() deleteWithLog()
return ErrProjectInviteInvalid.New(projInviteInvalidErrMsg) 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) { if err != nil && !errs.Is(err, sql.ErrNoRows) {
return nil, Error.Wrap(err) 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) return nil, ErrProjectInviteActive.New(projInviteActiveErrMsg, invitedUser.Email)
} }
users = append(users, invitedUser) users = append(users, invitedUser)
@ -3662,6 +3662,11 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID,
return invites, nil 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. // GetInviteByToken returns a project invite given an invite token.
func (s *Service) GetInviteByToken(ctx context.Context, token string) (invite *ProjectInvitation, err error) { func (s *Service) GetInviteByToken(ctx context.Context, token string) (invite *ProjectInvitation, err error) {
defer mon.Task()(&ctx)(&err) 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) return nil, ErrProjectInviteInvalid.New(projInviteInvalidErrMsg)
} }
if time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) { if s.IsProjectInvitationExpired(invite) {
return nil, ErrProjectInviteInvalid.New(projInviteInvalidErrMsg) return nil, ErrProjectInviteInvalid.New(projInviteInvalidErrMsg)
} }

View File

@ -1987,10 +1987,11 @@ func TestProjectInvitations(t *testing.T) {
expireInvite := func(t *testing.T, ctx context.Context, invite *console.ProjectInvitation) { expireInvite := func(t *testing.T, ctx context.Context, invite *console.ProjectInvitation) {
createdAt := time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration) 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, CreatedAt: &createdAt,
}) })
require.NoError(t, err) require.NoError(t, err)
*invite = *newInvite
} }
t.Run("invite users", func(t *testing.T) { 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.True(t, console.ErrProjectInviteActive.Has(err))
require.Empty(t, invites) 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) 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}) invites, err = service.InviteProjectMembers(ctx2, project.ID, []string{user3.Email})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, invites, 1) require.Len(t, invites, 1)
require.Equal(t, user2.ID, *invites[0].InviterID) 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. // inviting a project member should fail.
_, err = service.InviteProjectMembers(ctx, project.ID, []string{user2.Email}) _, err = service.InviteProjectMembers(ctx, project.ID, []string{user2.Email})

View File

@ -64,7 +64,8 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
}, },
projectInvitations { projectInvitations {
email, email,
createdAt createdAt,
expired
}, },
search, search,
limit, limit,
@ -127,6 +128,7 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
projectMembersPage.projectInvitations = projectMembers.projectInvitations.map(key => new ProjectInvitationItemModel( projectMembersPage.projectInvitations = projectMembers.projectInvitations.map(key => new ProjectInvitationItemModel(
key.email, key.email,
new Date(key.createdAt), new Date(key.createdAt),
key.expired,
)); ));
projectMembersPage.search = projectMembers.search; projectMembersPage.search = projectMembers.search;

View File

@ -231,6 +231,7 @@ export class ProjectInvitationItemModel implements ProjectMemberItemModel {
public constructor( public constructor(
public email: string, public email: string,
public createdAt: Date, public createdAt: Date,
public expired: boolean,
) {} ) {}
/** /**