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

View File

@ -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

View File

@ -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)
}

View File

@ -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})

View File

@ -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;

View File

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