satellite/console: don't delete expired project invitations

This change removes instances of project invitation deletion due to
expiration because we now want such invitations to be accessible beyond
their expiration date. In the future, project members will be able to
view and resend expired invitations within the Team page in the
satellite frontend.

References #5752

Change-Id: If24a9637945874d719b894a66c06f6e0e9805dfa
This commit is contained in:
Jeremy Wharton 2023-06-20 00:45:45 -05:00
parent b6026b9ff3
commit d18f4f7d99
7 changed files with 232 additions and 104 deletions

View File

@ -22,6 +22,8 @@ type ProjectInvitations interface {
GetByProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectInvitation, error) GetByProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectInvitation, error)
// GetByEmail returns all of the project member invitations for the specified email address. // GetByEmail returns all of the project member invitations for the specified email address.
GetByEmail(ctx context.Context, email string) ([]ProjectInvitation, error) GetByEmail(ctx context.Context, email string) ([]ProjectInvitation, error)
// Update updates the project member invitation specified by the given project ID and email address.
Update(ctx context.Context, projectID uuid.UUID, email string, request UpdateProjectInvitationRequest) (*ProjectInvitation, error)
// Delete removes a project member invitation from the database. // Delete removes a project member invitation from the database.
Delete(ctx context.Context, projectID uuid.UUID, email string) error Delete(ctx context.Context, projectID uuid.UUID, email string) error
// DeleteBefore deletes project member invitations created prior to some time from the database. // DeleteBefore deletes project member invitations created prior to some time from the database.
@ -35,3 +37,9 @@ type ProjectInvitation struct {
InviterID *uuid.UUID InviterID *uuid.UUID
CreatedAt time.Time CreatedAt time.Time
} }
// UpdateProjectInvitationRequest contains all fields which may be updated by ProjectInvitations.Update.
type UpdateProjectInvitationRequest struct {
CreatedAt *time.Time
InviterID *uuid.UUID
}

View File

@ -75,7 +75,7 @@ const (
projInviteInvalidErrMsg = "The invitation has expired or is invalid" projInviteInvalidErrMsg = "The invitation has expired or is invalid"
projInviteAlreadyMemberErrMsg = "You are already a member of the project" projInviteAlreadyMemberErrMsg = "You are already a member of the project"
projInviteResponseInvalidErrMsg = "Invalid project member invitation response" projInviteResponseInvalidErrMsg = "Invalid project member invitation response"
projInviteExistsErrMsg = "User has already been invited" projInviteActiveErrMsg = "The invitation for '%s' has not expired yet"
) )
var ( var (
@ -143,8 +143,8 @@ var (
// or has expired. // or has expired.
ErrProjectInviteInvalid = errs.Class("invalid project invitation") ErrProjectInviteInvalid = errs.Class("invalid project invitation")
// ErrProjectInviteExists occurs when a user is invited to a project they've already been invited to. // ErrProjectInviteActive occurs when trying to reinvite a user whose invitation hasn't expired yet.
ErrProjectInviteExists = errs.Class("user already invited to project") ErrProjectInviteActive = errs.Class("project invitation active")
) )
// Service is handling accounts related logic. // Service is handling accounts related logic.
@ -3470,26 +3470,10 @@ func (s *Service) GetUserProjectInvitations(ctx context.Context) (_ []ProjectInv
} }
var active []ProjectInvitation var active []ProjectInvitation
var deleteErrs []error
var expiredIDs []string
for _, invite := range invites { for _, invite := range invites {
if time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) { if !time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) {
err := s.store.ProjectInvitations().Delete(ctx, invite.ProjectID, invite.Email) active = append(active, invite)
if err != nil {
deleteErrs = append(deleteErrs, err)
expiredIDs = append(expiredIDs, invite.ProjectID.String())
}
continue
} }
active = append(active, invite)
}
if len(deleteErrs) != 0 {
s.log.Warn("error deleting expired project invitations",
zap.Errors("errors", deleteErrs),
zap.String("email", user.Email),
zap.Strings("projectIDs", expiredIDs),
)
} }
return active, nil return active, nil
@ -3580,6 +3564,7 @@ func (s *Service) RespondToProjectInvitation(ctx context.Context, projectID uuid
} }
// InviteProjectMembers invites users by email to given project. // InviteProjectMembers invites users by email to given project.
// If an invitation already exists and has expired, it will be replaced and the user will be sent a new email.
// Email addresses not belonging to a user are ignored. // Email addresses not belonging to a user are ignored.
// projectID here may be project.PublicID or project.ID. // projectID here may be project.PublicID or project.ID.
func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID, emails []string) (invites []ProjectInvitation, err error) { func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID, emails []string) (invites []ProjectInvitation, err error) {
@ -3611,24 +3596,13 @@ 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 && !time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) {
// delete expired invite return nil, ErrProjectInviteActive.New(projInviteActiveErrMsg, invitedUser.Email)
err := s.store.ProjectInvitations().Delete(ctx, projectID, invitedUser.Email)
if err != nil {
s.log.Warn("error deleting project invitation",
zap.Error(err),
zap.String("email", invitedUser.Email),
zap.String("projectID", projectID.String()),
)
}
} else if invite != nil && !time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) {
return nil, ErrProjectInviteExists.New(projInviteExistsErrMsg)
} }
users = append(users, invitedUser) users = append(users, invitedUser)
} else if !errs.Is(err, sql.ErrNoRows) { } else if !errs.Is(err, sql.ErrNoRows) {
return nil, Error.Wrap(err) return nil, Error.Wrap(err)
} }
} }
inviteTokens := make(map[string]string) inviteTokens := make(map[string]string)
@ -3641,11 +3615,17 @@ func (s *Service) InviteProjectMembers(ctx context.Context, projectID uuid.UUID,
InviterID: &user.ID, InviterID: &user.ID,
}) })
if err != nil { if err != nil {
if dbx.IsConstraintError(err) { if !dbx.IsConstraintError(err) {
// should not happen, but just in case. return err
return errs.New("%s is already invited", invited.Email) }
now := time.Now()
invite, err = tx.ProjectInvitations().Update(ctx, projectID, invited.Email, UpdateProjectInvitationRequest{
CreatedAt: &now,
InviterID: &user.ID,
})
if err != nil {
return err
} }
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.Add(s.config.ProjectInvitationExpiration))
if err != nil { if err != nil {
@ -3707,14 +3687,6 @@ 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 time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) {
err = s.store.ProjectInvitations().Delete(ctx, invite.ProjectID, invite.Email)
if err != nil {
s.log.Warn("error deleting expired project invitations",
zap.Error(err),
zap.String("email", email),
zap.String("projectID", project.ID.String()),
)
}
return nil, ErrProjectInviteInvalid.New(projInviteInvalidErrMsg) return nil, ErrProjectInviteInvalid.New(projInviteInvalidErrMsg)
} }

View File

@ -11,7 +11,6 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"sort" "sort"
"strings"
"testing" "testing"
"time" "time"
@ -1975,23 +1974,7 @@ func TestProjectInvitations(t *testing.T) {
return project return project
} }
setInviteDate := func(ctx context.Context, invite *console.ProjectInvitation, createdAt time.Time) *console.ProjectInvitation { addInvite := func(t *testing.T, ctx context.Context, project *console.Project, email string) *console.ProjectInvitation {
result, err := sat.DB.Testing().RawDB().ExecContext(ctx,
"UPDATE project_invitations SET created_at = $1 WHERE project_id = $2 AND email = $3",
createdAt, invite.ProjectID, strings.ToUpper(invite.Email),
)
require.NoError(t, err)
count, err := result.RowsAffected()
require.NoError(t, err)
require.EqualValues(t, 1, count)
invite, err = sat.DB.Console().ProjectInvitations().Get(ctx, invite.ProjectID, invite.Email)
require.NoError(t, err)
return invite
}
addInvite := func(t *testing.T, ctx context.Context, project *console.Project, email string, createdAt time.Time) *console.ProjectInvitation {
invite, err := sat.DB.Console().ProjectInvitations().Insert(ctx, &console.ProjectInvitation{ invite, err := sat.DB.Console().ProjectInvitations().Insert(ctx, &console.ProjectInvitation{
ProjectID: project.ID, ProjectID: project.ID,
Email: email, Email: email,
@ -1999,7 +1982,15 @@ func TestProjectInvitations(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
return setInviteDate(ctx, invite, createdAt) return invite
}
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{
CreatedAt: &createdAt,
})
require.NoError(t, err)
} }
t.Run("invite users", func(t *testing.T) { t.Run("invite users", func(t *testing.T) {
@ -2026,19 +2017,7 @@ func TestProjectInvitations(t *testing.T) {
invites, err = service.GetUserProjectInvitations(ctx3) invites, err = service.GetUserProjectInvitations(ctx3)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, invites, 1) require.Len(t, invites, 1)
invite := invites[0] user3Invite := invites[0]
// inviting the same user again should fail if existing invite hasn't expired.
_, err = service.InviteProjectMembers(ctx, project.ID, []string{user3.Email})
require.Error(t, err)
// expire the invitation.
setInviteDate(ctx, &invite, time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration))
// inviting the same user again should succeed because the existing invite has expired.
invites, err = service.InviteProjectMembers(ctx, project.ID, []string{user3.Email})
require.NoError(t, err)
require.Len(t, invites, 1)
// prevent unauthorized users from inviting others (user2 is not a member of the project yet). // prevent unauthorized users from inviting others (user2 is not a member of the project yet).
_, err = service.InviteProjectMembers(ctx2, project.ID, []string{"other@mail.com"}) _, err = service.InviteProjectMembers(ctx2, project.ID, []string{"other@mail.com"})
@ -2047,9 +2026,18 @@ func TestProjectInvitations(t *testing.T) {
require.NoError(t, service.RespondToProjectInvitation(ctx2, project.ID, console.ProjectInvitationAccept)) require.NoError(t, service.RespondToProjectInvitation(ctx2, project.ID, console.ProjectInvitationAccept))
// now that user2 is a member, they can invite others. // resending an active invitation should fail.
_, err = service.InviteProjectMembers(ctx2, project.ID, []string{"other@mail.com"}) invites, err = service.InviteProjectMembers(ctx2, project.ID, []string{user3.Email})
require.True(t, console.ErrProjectInviteActive.Has(err))
require.Empty(t, invites)
// resending an expired invitation should succeed.
expireInvite(t, ctx, &user3Invite)
invites, err = service.InviteProjectMembers(ctx2, project.ID, []string{user3.Email})
require.NoError(t, err) 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))
// 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})
@ -2063,7 +2051,7 @@ func TestProjectInvitations(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, invites) require.Empty(t, invites)
invite := addInvite(t, ctx, addProject(t, ctx), user.Email, time.Now()) invite := addInvite(t, ctx, addProject(t, ctx), user.Email)
invites, err = service.GetUserProjectInvitations(ctx) invites, err = service.GetUserProjectInvitations(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, invites, 1) require.Len(t, invites, 1)
@ -2072,7 +2060,7 @@ func TestProjectInvitations(t *testing.T) {
require.Equal(t, invite.InviterID, invites[0].InviterID) require.Equal(t, invite.InviterID, invites[0].InviterID)
require.WithinDuration(t, invite.CreatedAt, invites[0].CreatedAt, time.Second) require.WithinDuration(t, invite.CreatedAt, invites[0].CreatedAt, time.Second)
setInviteDate(ctx, invite, time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration)) expireInvite(t, ctx, &invites[0])
invites, err = service.GetUserProjectInvitations(ctx) invites, err = service.GetUserProjectInvitations(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, invites) require.Empty(t, invites)
@ -2118,7 +2106,7 @@ func TestProjectInvitations(t *testing.T) {
project, err := sat.AddProject(ctx, owner.ID, "Test Project") project, err := sat.AddProject(ctx, owner.ID, "Test Project")
require.NoError(t, err) require.NoError(t, err)
invite := addInvite(t, ctx, project, user.Email, time.Now()) invite := addInvite(t, ctx, project, user.Email)
someToken, err := service.CreateInviteToken(ctx, project.PublicID, "some@email.com", invite.CreatedAt) someToken, err := service.CreateInviteToken(ctx, project.PublicID, "some@email.com", invite.CreatedAt)
require.NoError(t, err) require.NoError(t, err)
@ -2136,7 +2124,7 @@ func TestProjectInvitations(t *testing.T) {
require.NotNil(t, inviteFromToken) require.NotNil(t, inviteFromToken)
require.Equal(t, invite, inviteFromToken) require.Equal(t, invite, inviteFromToken)
setInviteDate(ctx, invite, time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration)) expireInvite(t, ctx, invite)
invites, err := service.GetUserProjectInvitations(ctx) invites, err := service.GetUserProjectInvitations(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, invites) require.Empty(t, invites)
@ -2158,11 +2146,12 @@ func TestProjectInvitations(t *testing.T) {
user, ctx := getUserAndCtx(t) user, ctx := getUserAndCtx(t)
proj := addProject(t, ctx) proj := addProject(t, ctx)
addInvite(t, ctx, proj, user.Email, time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration)) invite := addInvite(t, ctx, proj, user.Email)
expireInvite(t, ctx, invite)
err := service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept) err := service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept)
require.True(t, console.ErrProjectInviteInvalid.Has(err)) require.True(t, console.ErrProjectInviteInvalid.Has(err))
addInvite(t, ctx, proj, user.Email, time.Now()) addInvite(t, ctx, proj, user.Email)
require.NoError(t, service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept)) require.NoError(t, service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept))
invites, err := service.GetUserProjectInvitations(ctx) invites, err := service.GetUserProjectInvitations(ctx)
@ -2186,7 +2175,7 @@ func TestProjectInvitations(t *testing.T) {
user, ctx := getUserAndCtx(t) user, ctx := getUserAndCtx(t)
proj := addProject(t, ctx) proj := addProject(t, ctx)
addInvite(t, ctx, proj, user.Email, time.Now()) addInvite(t, ctx, proj, user.Email)
require.NoError(t, service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationDecline)) require.NoError(t, service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationDecline))
invites, err := service.GetUserProjectInvitations(ctx) invites, err := service.GetUserProjectInvitations(ctx)

View File

@ -164,9 +164,9 @@ model project_invitation (
// See satellitedb.normalizeEmail for details. // See satellitedb.normalizeEmail for details.
field email text field email text
// inviter_id is the ID of the user who sent the invitation. // inviter_id is the ID of the user who sent the invitation.
field inviter_id user.id setnull ( nullable ) field inviter_id user.id setnull ( nullable, updatable )
// created_at is the time that the invitation was created. // created_at is the time that the invitation was created.
field created_at timestamp ( autoinsert ) field created_at timestamp ( autoinsert, updatable )
) )
create project_invitation ( ) create project_invitation ( )
@ -187,6 +187,11 @@ read all (
where project_invitation.project_id = ? where project_invitation.project_id = ?
) )
update project_invitation (
where project_invitation.project_id = ?
where project_invitation.email = ?
)
delete project_invitation ( delete project_invitation (
where project_invitation.project_id = ? where project_invitation.project_id = ?
where project_invitation.email = ? where project_invitation.email = ?

View File

@ -11263,6 +11263,8 @@ type ProjectInvitation_Create_Fields struct {
} }
type ProjectInvitation_Update_Fields struct { type ProjectInvitation_Update_Fields struct {
InviterId ProjectInvitation_InviterId_Field
CreatedAt ProjectInvitation_CreatedAt_Field
} }
type ProjectInvitation_ProjectId_Field struct { type ProjectInvitation_ProjectId_Field struct {
@ -18340,6 +18342,53 @@ func (obj *pgxImpl) Update_Project_By_Id(ctx context.Context,
return project, nil return project, nil
} }
func (obj *pgxImpl) Update_ProjectInvitation_By_ProjectId_And_Email(ctx context.Context,
project_invitation_project_id ProjectInvitation_ProjectId_Field,
project_invitation_email ProjectInvitation_Email_Field,
update ProjectInvitation_Update_Fields) (
project_invitation *ProjectInvitation, err error) {
defer mon.Task()(&ctx)(&err)
var __sets = &__sqlbundle_Hole{}
var __embed_stmt = __sqlbundle_Literals{Join: "", SQLs: []__sqlbundle_SQL{__sqlbundle_Literal("UPDATE project_invitations SET "), __sets, __sqlbundle_Literal(" WHERE project_invitations.project_id = ? AND project_invitations.email = ? RETURNING project_invitations.project_id, project_invitations.email, project_invitations.inviter_id, project_invitations.created_at")}}
__sets_sql := __sqlbundle_Literals{Join: ", "}
var __values []interface{}
var __args []interface{}
if update.InviterId._set {
__values = append(__values, update.InviterId.value())
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("inviter_id = ?"))
}
if update.CreatedAt._set {
__values = append(__values, update.CreatedAt.value())
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("created_at = ?"))
}
if len(__sets_sql.SQLs) == 0 {
return nil, emptyUpdate()
}
__args = append(__args, project_invitation_project_id.value(), project_invitation_email.value())
__values = append(__values, __args...)
__sets.SQL = __sets_sql
var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt)
obj.logStmt(__stmt, __values...)
project_invitation = &ProjectInvitation{}
err = obj.queryRowContext(ctx, __stmt, __values...).Scan(&project_invitation.ProjectId, &project_invitation.Email, &project_invitation.InviterId, &project_invitation.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, obj.makeErr(err)
}
return project_invitation, nil
}
func (obj *pgxImpl) UpdateNoReturn_ApiKey_By_Id(ctx context.Context, func (obj *pgxImpl) UpdateNoReturn_ApiKey_By_Id(ctx context.Context,
api_key_id ApiKey_Id_Field, api_key_id ApiKey_Id_Field,
update ApiKey_Update_Fields) ( update ApiKey_Update_Fields) (
@ -26300,6 +26349,53 @@ func (obj *pgxcockroachImpl) Update_Project_By_Id(ctx context.Context,
return project, nil return project, nil
} }
func (obj *pgxcockroachImpl) Update_ProjectInvitation_By_ProjectId_And_Email(ctx context.Context,
project_invitation_project_id ProjectInvitation_ProjectId_Field,
project_invitation_email ProjectInvitation_Email_Field,
update ProjectInvitation_Update_Fields) (
project_invitation *ProjectInvitation, err error) {
defer mon.Task()(&ctx)(&err)
var __sets = &__sqlbundle_Hole{}
var __embed_stmt = __sqlbundle_Literals{Join: "", SQLs: []__sqlbundle_SQL{__sqlbundle_Literal("UPDATE project_invitations SET "), __sets, __sqlbundle_Literal(" WHERE project_invitations.project_id = ? AND project_invitations.email = ? RETURNING project_invitations.project_id, project_invitations.email, project_invitations.inviter_id, project_invitations.created_at")}}
__sets_sql := __sqlbundle_Literals{Join: ", "}
var __values []interface{}
var __args []interface{}
if update.InviterId._set {
__values = append(__values, update.InviterId.value())
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("inviter_id = ?"))
}
if update.CreatedAt._set {
__values = append(__values, update.CreatedAt.value())
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("created_at = ?"))
}
if len(__sets_sql.SQLs) == 0 {
return nil, emptyUpdate()
}
__args = append(__args, project_invitation_project_id.value(), project_invitation_email.value())
__values = append(__values, __args...)
__sets.SQL = __sets_sql
var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt)
obj.logStmt(__stmt, __values...)
project_invitation = &ProjectInvitation{}
err = obj.queryRowContext(ctx, __stmt, __values...).Scan(&project_invitation.ProjectId, &project_invitation.Email, &project_invitation.InviterId, &project_invitation.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, obj.makeErr(err)
}
return project_invitation, nil
}
func (obj *pgxcockroachImpl) UpdateNoReturn_ApiKey_By_Id(ctx context.Context, func (obj *pgxcockroachImpl) UpdateNoReturn_ApiKey_By_Id(ctx context.Context,
api_key_id ApiKey_Id_Field, api_key_id ApiKey_Id_Field,
update ApiKey_Update_Fields) ( update ApiKey_Update_Fields) (
@ -29806,6 +29902,18 @@ func (rx *Rx) Update_Node_By_Id(ctx context.Context,
return tx.Update_Node_By_Id(ctx, node_id, update) return tx.Update_Node_By_Id(ctx, node_id, update)
} }
func (rx *Rx) Update_ProjectInvitation_By_ProjectId_And_Email(ctx context.Context,
project_invitation_project_id ProjectInvitation_ProjectId_Field,
project_invitation_email ProjectInvitation_Email_Field,
update ProjectInvitation_Update_Fields) (
project_invitation *ProjectInvitation, err error) {
var tx *Tx
if tx, err = rx.getTx(ctx); err != nil {
return
}
return tx.Update_ProjectInvitation_By_ProjectId_And_Email(ctx, project_invitation_project_id, project_invitation_email, update)
}
func (rx *Rx) Update_Project_By_Id(ctx context.Context, func (rx *Rx) Update_Project_By_Id(ctx context.Context,
project_id Project_Id_Field, project_id Project_Id_Field,
update Project_Update_Fields) ( update Project_Update_Fields) (
@ -30793,6 +30901,12 @@ type Methods interface {
update Node_Update_Fields) ( update Node_Update_Fields) (
node *Node, err error) node *Node, err error)
Update_ProjectInvitation_By_ProjectId_And_Email(ctx context.Context,
project_invitation_project_id ProjectInvitation_ProjectId_Field,
project_invitation_email ProjectInvitation_Email_Field,
update ProjectInvitation_Update_Fields) (
project_invitation *ProjectInvitation, err error)
Update_Project_By_Id(ctx context.Context, Update_Project_By_Id(ctx context.Context,
project_id Project_Id_Field, project_id Project_Id_Field,
update Project_Update_Fields) ( update Project_Update_Fields) (

View File

@ -87,6 +87,30 @@ func (invites *projectInvitations) GetByEmail(ctx context.Context, email string)
return projectInvitationSliceFromDBX(dbxInvites) return projectInvitationSliceFromDBX(dbxInvites)
} }
// Update updates the project member invitation specified by the given project ID and email address.
func (invites *projectInvitations) Update(ctx context.Context, projectID uuid.UUID, email string, request console.UpdateProjectInvitationRequest) (_ *console.ProjectInvitation, err error) {
defer mon.Task()(&ctx)(&err)
update := dbx.ProjectInvitation_Update_Fields{}
if request.CreatedAt != nil {
update.CreatedAt = dbx.ProjectInvitation_CreatedAt(*request.CreatedAt)
}
if request.InviterID != nil {
update.InviterId = dbx.ProjectInvitation_InviterId((*request.InviterID)[:])
}
dbxInvite, err := invites.db.Update_ProjectInvitation_By_ProjectId_And_Email(ctx,
dbx.ProjectInvitation_ProjectId(projectID[:]),
dbx.ProjectInvitation_Email(normalizeEmail(email)),
update,
)
if err != nil {
return nil, err
}
return projectInvitationFromDBX(dbxInvite)
}
// Delete removes a project member invitation from the database. // Delete removes a project member invitation from the database.
func (invites *projectInvitations) Delete(ctx context.Context, projectID uuid.UUID, email string) (err error) { func (invites *projectInvitations) Delete(ctx context.Context, projectID uuid.UUID, email string) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)

View File

@ -123,6 +123,27 @@ func TestProjectInvitations(t *testing.T) {
require.Nil(t, invite.InviterID) require.Nil(t, invite.InviterID)
}) })
t.Run("update invitation", func(t *testing.T) {
ctx := testcontext.New(t)
req := console.UpdateProjectInvitationRequest{}
newCreatedAt := invite.CreatedAt.Add(time.Hour)
req.CreatedAt = &newCreatedAt
newInvite, err := invitesDB.Update(ctx, projID, email, req)
require.NoError(t, err)
require.Equal(t, newCreatedAt, newInvite.CreatedAt)
inviter, err := db.Console().Users().Insert(ctx, &console.User{
ID: testrand.UUID(),
PasswordHash: testrand.Bytes(8),
})
require.NoError(t, err)
req.InviterID = &inviter.ID
newInvite, err = invitesDB.Update(ctx, projID, email, req)
require.NoError(t, err)
require.Equal(t, inviter.ID, *newInvite.InviterID)
})
t.Run("delete invitation", func(t *testing.T) { t.Run("delete invitation", func(t *testing.T) {
ctx := testcontext.New(t) ctx := testcontext.New(t)
@ -156,13 +177,12 @@ func TestDeleteBefore(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) { satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
invitesDB := db.Console().ProjectInvitations() invitesDB := db.Console().ProjectInvitations()
now := time.Now()
// Only positive page sizes should be allowed. // Only positive page sizes should be allowed.
require.Error(t, invitesDB.DeleteBefore(ctx, time.Time{}, 0, 0)) require.Error(t, invitesDB.DeleteBefore(ctx, time.Time{}, 0, 0))
require.Error(t, invitesDB.DeleteBefore(ctx, time.Time{}, 0, -1)) require.Error(t, invitesDB.DeleteBefore(ctx, time.Time{}, 0, -1))
createInvite := func(createdAt time.Time) *console.ProjectInvitation { createInvite := func() *console.ProjectInvitation {
projID := testrand.UUID() projID := testrand.UUID()
_, err := db.Console().Projects().Insert(ctx, &console.Project{ID: projID}) _, err := db.Console().Projects().Insert(ctx, &console.Project{ID: projID})
require.NoError(t, err) require.NoError(t, err)
@ -170,26 +190,22 @@ func TestDeleteBefore(t *testing.T) {
invite, err := invitesDB.Insert(ctx, &console.ProjectInvitation{ProjectID: projID}) invite, err := invitesDB.Insert(ctx, &console.ProjectInvitation{ProjectID: projID})
require.NoError(t, err) require.NoError(t, err)
result, err := db.Testing().RawDB().ExecContext(ctx,
"UPDATE project_invitations SET created_at = $1 WHERE project_id = $2",
createdAt, invite.ProjectID,
)
require.NoError(t, err)
count, err := result.RowsAffected()
require.NoError(t, err)
require.EqualValues(t, 1, count)
return invite return invite
} }
newInvite := createInvite(now) newInvite := createInvite()
oldInvite := createInvite(expiration.Add(-time.Second))
oldInvite := createInvite()
oldCreatedAt := expiration.Add(-time.Second)
oldInvite, err := invitesDB.Update(ctx, oldInvite.ProjectID, oldInvite.Email, console.UpdateProjectInvitationRequest{
CreatedAt: &oldCreatedAt,
})
require.NoError(t, err)
require.NoError(t, invitesDB.DeleteBefore(ctx, expiration, 0, 1)) require.NoError(t, invitesDB.DeleteBefore(ctx, expiration, 0, 1))
// Ensure that the old invitation record was deleted and the other remains. // Ensure that the old invitation record was deleted and the other remains.
_, err := invitesDB.Get(ctx, oldInvite.ProjectID, oldInvite.Email) _, err = invitesDB.Get(ctx, oldInvite.ProjectID, oldInvite.Email)
require.ErrorIs(t, err, sql.ErrNoRows) require.ErrorIs(t, err, sql.ErrNoRows)
_, err = invitesDB.Get(ctx, newInvite.ProjectID, newInvite.Email) _, err = invitesDB.Get(ctx, newInvite.ProjectID, newInvite.Email)
require.NoError(t, err) require.NoError(t, err)