diff --git a/api/services/auth.go b/api/services/auth.go index ed42934ab1a..f32ea800a64 100644 --- a/api/services/auth.go +++ b/api/services/auth.go @@ -309,10 +309,8 @@ func (s *service) AuthLocalUser(ctx context.Context, req *requests.AuthLocalUser tenantID := "" role := "" - // Populate the tenant and role when the user is associated with a namespace. If the member status is pending, we - // ignore the namespace. if ns, _ := s.store.NamespaceGetPreferred(ctx, user.ID); ns != nil && ns.TenantID != "" { - if m, _ := ns.FindMember(user.ID); m.Status != models.MemberStatusPending { + if m, _ := ns.FindMember(user.ID); m != nil { tenantID = ns.TenantID role = m.Role.String() } @@ -393,10 +391,8 @@ func (s *service) CreateUserToken(ctx context.Context, req *requests.CreateUserT return nil, NewErrNamespaceMemberNotFound(user.ID, nil) } - if member.Status != models.MemberStatusPending { - tenantID = namespace.TenantID - role = member.Role.String() - } + tenantID = namespace.TenantID + role = member.Role.String() default: namespace, err := s.store.NamespaceResolve(ctx, store.NamespaceTenantIDResolver, req.TenantID) if err != nil { @@ -408,10 +404,6 @@ func (s *service) CreateUserToken(ctx context.Context, req *requests.CreateUserT return nil, NewErrNamespaceMemberNotFound(user.ID, nil) } - if member.Status == models.MemberStatusPending { - return nil, NewErrNamespaceMemberNotFound(user.ID, nil) - } - tenantID = namespace.TenantID role = member.Role.String() diff --git a/api/services/auth_test.go b/api/services/auth_test.go index 90ad3112183..8f818ec4f66 100644 --- a/api/services/auth_test.go +++ b/api/services/auth_test.go @@ -1890,134 +1890,6 @@ func TestService_AuthLocalUser(t *testing.T) { err: nil, }, }, - { - description: "succeeds to authenticate with a namespace (and member status 'pending')", - sourceIP: "127.0.0.1", - req: &requests.AuthLocalUser{ - Identifier: "john_doe", - Password: "secret", - }, - requiredMocks: func() { - user := &models.User{ - ID: "65fdd16b5f62f93184ec8a39", - Origin: models.UserOriginLocal, - Status: models.UserStatusConfirmed, - LastLogin: now, - MFA: models.UserMFA{ - Enabled: false, - }, - UserData: models.UserData{ - Username: "john_doe", - Email: "john.doe@test.com", - Name: "john doe", - }, - Password: models.UserPassword{ - Hash: "$2a$10$V/6N1wsjheBVvWosPfv02uf4WAOb9lmp8YWQCIa2UYuFV4OJby7Yi", - }, - Preferences: models.UserPreferences{ - PreferredNamespace: "00000000-0000-4000-0000-000000000000", - AuthMethods: []models.UserAuthMethod{models.UserAuthMethodLocal}, - }, - } - updatedUser := &models.User{ - ID: "65fdd16b5f62f93184ec8a39", - Origin: models.UserOriginLocal, - Status: models.UserStatusConfirmed, - LastLogin: now, - MFA: models.UserMFA{ - Enabled: false, - }, - UserData: models.UserData{ - Username: "john_doe", - Email: "john.doe@test.com", - Name: "john doe", - }, - Password: models.UserPassword{ - Hash: "$2a$10$V/6N1wsjheBVvWosPfv02uf4WAOb9lmp8YWQCIa2UYuFV4OJby7Yi", - }, - Preferences: models.UserPreferences{ - PreferredNamespace: "", - AuthMethods: []models.UserAuthMethod{models.UserAuthMethodLocal}, - }, - } - - mock. - On("SystemGet", ctx). - Return( - &models.System{ - Authentication: &models.SystemAuthentication{ - Local: &models.SystemAuthenticationLocal{ - Enabled: true, - }, - }, - }, - nil, - ). - Once() - mock. - On("UserResolve", ctx, store.UserUsernameResolver, "john_doe"). - Return(user, nil). - Once() - cacheMock. - On("HasAccountLockout", ctx, "127.0.0.1", "65fdd16b5f62f93184ec8a39"). - Return(int64(0), 0, nil). - Once() - hashMock. - On("CompareWith", "secret", "$2a$10$V/6N1wsjheBVvWosPfv02uf4WAOb9lmp8YWQCIa2UYuFV4OJby7Yi"). - Return(true). - Once() - cacheMock. - On("ResetLoginAttempts", ctx, "127.0.0.1", "65fdd16b5f62f93184ec8a39"). - Return(nil). - Once() - - ns := &models.Namespace{ - TenantID: "00000000-0000-4000-0000-000000000000", - Members: []models.Member{ - { - ID: "65fdd16b5f62f93184ec8a39", - Role: "owner", - Status: models.MemberStatusPending, - }, - }, - } - - mock. - On("NamespaceGetPreferred", ctx, "65fdd16b5f62f93184ec8a39"). - Return(ns, nil). - Once() - - clockMock := new(clockmock.Clock) - clock.DefaultBackend = clockMock - clockMock.On("Now").Return(now) - - cacheMock. - On("Set", ctx, "token_65fdd16b5f62f93184ec8a39", testifymock.Anything, time.Hour*72). - Return(nil). - Once() - - mock. - On("UserUpdate", ctx, updatedUser). - Return(nil). - Once() - }, - expected: Expected{ - res: &models.UserAuthResponse{ - ID: "65fdd16b5f62f93184ec8a39", - Origin: models.UserOriginLocal.String(), - AuthMethods: []models.UserAuthMethod{models.UserAuthMethodLocal}, - Name: "john doe", - User: "john_doe", - Email: "john.doe@test.com", - Tenant: "", - Role: "", - Token: "must ignore", - }, - lockout: 0, - mfaToken: "", - err: nil, - }, - }, { description: "succeeds to authenticate with a namespace (and empty preferred namespace)", sourceIP: "127.0.0.1", @@ -2391,57 +2263,6 @@ func TestCreateUserToken(t *testing.T) { err: NewErrNamespaceMemberNotFound("000000000000000000000000", nil), }, }, - { - description: "[with-tenant] fails when user membership is pending", - req: &requests.CreateUserToken{UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000"}, - requiredMocks: func(ctx context.Context) { - storeMock. - On("UserResolve", ctx, store.UserIDResolver, "000000000000000000000000"). - Return( - &models.User{ - ID: "000000000000000000000000", - Status: models.UserStatusConfirmed, - LastLogin: now, - MFA: models.UserMFA{ - Enabled: false, - }, - UserData: models.UserData{ - Username: "john_doe", - Email: "john.doe@test.com", - Name: "john doe", - }, - Password: models.UserPassword{ - Hash: "$2a$10$V/6N1wsjheBVvWosPfv02uf4WAOb9lmp8YWQCIa2UYuFV4OJby7Yi", - }, - Preferences: models.UserPreferences{ - PreferredNamespace: "", - }, - }, - nil, - ). - Once() - storeMock. - On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "00000000-0000-4000-0000-000000000000"). - Return( - &models.Namespace{ - TenantID: "00000000-0000-4000-0000-000000000000", - Members: []models.Member{ - { - ID: "000000000000000000000000", - Role: "administrator", - Status: models.MemberStatusPending, - }, - }, - }, - nil, - ). - Once() - }, - expected: Expected{ - res: nil, - err: NewErrNamespaceMemberNotFound("000000000000000000000000", nil), - }, - }, { description: "[with-tenant] succeeds", req: &requests.CreateUserToken{UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000"}, @@ -2496,9 +2317,8 @@ func TestCreateUserToken(t *testing.T) { TenantID: "00000000-0000-4000-0000-000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: "owner", - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: "owner", }, }, }, @@ -2566,9 +2386,8 @@ func TestCreateUserToken(t *testing.T) { TenantID: "00000000-0000-4000-0000-000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: "owner", - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: "owner", }, }, }, diff --git a/api/services/member.go b/api/services/member.go index ba721507c20..1f6285c07a7 100644 --- a/api/services/member.go +++ b/api/services/member.go @@ -23,10 +23,10 @@ type MemberService interface { // AddNamespaceMember adds a member to a namespace. // - // In cloud environments, the member is assigned a [MemberStatusPending] status until they accept the invite via + // In cloud environments, a membership invitation is created with pending status until they accept the invite via // an invitation email. If the target user does not exist, the email will redirect them to the registration page, - // and the invite can be accepted after finishing. In community and enterprise environments, the status is set to - // [MemberStatusAccepted] without sending an email. + // and the invite can be accepted after finishing. In community and enterprise environments, the member is added + // directly to the namespace without sending an email. // // The role assigned to the new member must not grant more authority than the user adding them (e.g., // an administrator cannot add a member with a higher role such as an owner). Owners cannot be created. @@ -35,11 +35,15 @@ type MemberService interface { AddNamespaceMember(ctx context.Context, req *requests.NamespaceAddMember) (*models.Namespace, error) // UpdateNamespaceMember updates a member with the specified ID in the specified namespace. The member's role cannot - // have more authority than the user who is updating the member; owners cannot be created. It returns an error, if any. + // have more authority than the user who is updating the member; owners cannot be created. + // + // It returns an error, if any. UpdateNamespaceMember(ctx context.Context, req *requests.NamespaceUpdateMember) error // RemoveNamespaceMember removes a specified member from a namespace. The action must be performed by a user with higher - // authority than the target member. Owners cannot be removed. Returns the updated namespace and an error, if any. + // authority than the target member. Owners cannot be removed. + // + // Returns the updated namespace and an error, if any. RemoveNamespaceMember(ctx context.Context, req *requests.NamespaceRemoveMember) (*models.Namespace, error) // LeaveNamespace allows an authenticated user to remove themselves from a namespace. Owners cannot leave a namespace. @@ -85,54 +89,81 @@ func (s *service) AddNamespaceMember(ctx context.Context, req *requests.Namespac } } - // In cloud instances, if a member exists and their status is pending and the expiration date is reached, - // we resend the invite instead of adding the member. - // In community and enterprise instances, a "duplicate" error is always returned, - // since the member will never be in a pending status. - // Otherwise, add the member "from scratch" - if m, ok := namespace.FindMember(passiveUser.ID); ok { - now := clock.Now() - - if !envs.IsCloud() || (m.Status != models.MemberStatusPending || !m.ExpiresAt.Before(now)) { - return nil, NewErrNamespaceMemberDuplicated(passiveUser.ID, nil) - } + if _, ok := namespace.FindMember(passiveUser.ID); ok { + return nil, NewErrNamespaceMemberDuplicated(passiveUser.ID, nil) + } - if err := s.store.WithTransaction(ctx, s.resendMemberInvite(m, req)); err != nil { - return nil, err - } + var callback store.TransactionCb + if !envs.IsCloud() { + callback = s.addMember(namespace, passiveUser.ID, req) } else { - if err := s.store.WithTransaction(ctx, s.addMember(passiveUser.ID, req)); err != nil { + invitation, err := s.store.MembershipInvitationResolve(ctx, req.TenantID, passiveUser.ID) + if err != nil && !errors.Is(err, store.ErrNoDocuments) { return nil, err } + + switch { + case invitation == nil, !invitation.IsPending(): + callback = s.addMember(namespace, passiveUser.ID, req) + case invitation.IsExpired(): + callback = s.resendMembershipInvite(invitation, req) + default: + return nil, NewErrNamespaceMemberDuplicated(passiveUser.ID, nil) + } } - return s.store.NamespaceResolve(ctx, store.NamespaceTenantIDResolver, req.TenantID) + if err := s.store.WithTransaction(ctx, callback); err != nil { + return nil, err + } + + n, err := s.store.NamespaceResolve(ctx, store.NamespaceTenantIDResolver, req.TenantID) + if err != nil { + return nil, err + } + + return n, nil } -// addMember returns a transaction callback that adds a member and sends an invite if the instance is cloud. -func (s *service) addMember(memberID string, req *requests.NamespaceAddMember) store.TransactionCb { +// addMember returns a transaction callback that adds a member to a namespace. +// +// In all environments, it creates a membership_invitation record for audit purposes: +// - Cloud: Creates pending invitation with expiration and sends email +// - Community/Enterprise: Creates accepted invitation and adds member directly to namespace +func (s *service) addMember(namespace *models.Namespace, userID string, req *requests.NamespaceAddMember) store.TransactionCb { return func(ctx context.Context) error { - member := &models.Member{ - ID: memberID, - AddedAt: clock.Now(), - Role: req.MemberRole, + now := clock.Now() + + invitation := &models.MembershipInvitation{ + TenantID: req.TenantID, + UserID: userID, + InvitedBy: namespace.Owner, + Role: req.MemberRole, + CreatedAt: now, + UpdatedAt: now, + StatusUpdatedAt: now, + Invitations: 1, } - // In cloud instances, the member must accept the invite before enter in the namespace. if envs.IsCloud() { - member.Status = models.MemberStatusPending - member.ExpiresAt = member.AddedAt.Add(7 * (24 * time.Hour)) - } else { - member.Status = models.MemberStatusAccepted - member.ExpiresAt = time.Time{} - } + expiresAt := now.Add(7 * (24 * time.Hour)) + invitation.Status = models.MembershipInvitationStatusPending + invitation.ExpiresAt = &expiresAt + if err := s.store.MembershipInvitationCreate(ctx, invitation); err != nil { + return err + } - if err := s.store.NamespaceCreateMembership(ctx, req.TenantID, member); err != nil { - return err - } + if err := s.client.InviteMember(ctx, req.TenantID, userID, req.FowardedHost); err != nil { + return err + } + } else { + invitation.Status = models.MembershipInvitationStatusAccepted + invitation.ExpiresAt = nil + if err := s.store.MembershipInvitationCreate(ctx, invitation); err != nil { + return err + } - if envs.IsCloud() { - if err := s.client.InviteMember(ctx, req.TenantID, member.ID, req.FowardedHost); err != nil { + member := &models.Member{ID: userID, AddedAt: now, Role: req.MemberRole} + if err := s.store.NamespaceCreateMembership(ctx, req.TenantID, member); err != nil { return err } } @@ -141,18 +172,27 @@ func (s *service) addMember(memberID string, req *requests.NamespaceAddMember) s } } -// resendMemberInvite returns a transaction callback that resends an invitation to the member with the -// specified ID. -func (s *service) resendMemberInvite(member *models.Member, req *requests.NamespaceAddMember) store.TransactionCb { +// resendMembershipInvite returns a transaction callback that resends a membership invitation. +// +// This function updates an existing invitation to pending status, extends the expiration date, +// increments the invitation counter, and sends a new invitation email (cloud only). +func (s *service) resendMembershipInvite(invitation *models.MembershipInvitation, req *requests.NamespaceAddMember) store.TransactionCb { return func(ctx context.Context) error { - member.ExpiresAt = clock.Now().Add(7 * (24 * time.Hour)) - member.Role = req.MemberRole + now := clock.Now() + + expiresAt := now.Add(7 * (24 * time.Hour)) + invitation.Status = models.MembershipInvitationStatusPending + invitation.Role = req.MemberRole + invitation.ExpiresAt = &expiresAt + invitation.UpdatedAt = now + invitation.StatusUpdatedAt = now + invitation.Invitations++ - if err := s.store.NamespaceUpdateMembership(ctx, req.TenantID, member); err != nil { + if err := s.store.MembershipInvitationUpdate(ctx, invitation); err != nil { return err } - return s.client.InviteMember(ctx, req.TenantID, member.ID, req.FowardedHost) + return s.client.InviteMember(ctx, req.TenantID, invitation.UserID, req.FowardedHost) } } diff --git a/api/services/member_test.go b/api/services/member_test.go index 0d97eb65cfe..db7c8f7c9b3 100644 --- a/api/services/member_test.go +++ b/api/services/member_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/mock" ) -func TestAddNamespaceMember(t *testing.T) { +func TestService_AddNamespaceMember(t *testing.T) { type Expected struct { namespace *models.Namespace err error @@ -44,7 +44,7 @@ func TestAddNamespaceMember(t *testing.T) { expected Expected }{ { - description: "fails when the namespace was not found", + description: "[community|enterprise|cloud] fails when the namespace was not found", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -64,7 +64,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "fails when the active member was not found", + description: "[community|enterprise|cloud] fails when the active member was not found", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -93,7 +93,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "fails when the active member is not on the namespace", + description: "[community|enterprise|cloud] fails when the active member is not on the namespace", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -125,7 +125,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "fails when the passive role's is owner", + description: "[community|enterprise|cloud] fails when the passive role's is owner", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -142,9 +142,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOperator, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOperator, }, }, }, nil). @@ -163,7 +162,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "fails when the active member's role cannot act over passive member's role", + description: "[community|enterprise|cloud] fails when the active member's role cannot act over passive member's role", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -180,9 +179,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOperator, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOperator, }, }, }, nil). @@ -201,7 +199,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "fails when passive member was not found", + description: "[community|enterprise] fails when passive member was not found", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -218,9 +216,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, nil). @@ -247,7 +244,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "fails when the member is duplicated without 'pending' status and expiration date not reached", + description: "[community|enterprise|cloud] fails when the member is duplicated", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -264,14 +261,12 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, { - ID: "000000000000000000000001", - Role: authorizer.RoleAdministrator, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000001", + Role: authorizer.RoleAdministrator, }, }, }, nil). @@ -290,10 +285,6 @@ func TestAddNamespaceMember(t *testing.T) { UserData: models.UserData{Username: "john_doe"}, }, nil). Once() - envMock. - On("Get", "SHELLHUB_CLOUD"). - Return("false"). - Once() }, expected: Expected{ namespace: nil, @@ -301,7 +292,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "[cloud] fails when the member is duplicated without 'pending' status and expiration date not reached", + description: "[cloud] fails when the member has pending invitation not expired", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -318,14 +309,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, - }, - { - ID: "000000000000000000000001", - Role: authorizer.RoleAdministrator, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, nil). @@ -348,6 +333,18 @@ func TestAddNamespaceMember(t *testing.T) { On("Get", "SHELLHUB_CLOUD"). Return("true"). Once() + storeMock. + On("MembershipInvitationResolve", ctx, "00000000-0000-4000-0000-000000000000", "000000000000000000000001"). + Return( + &models.MembershipInvitation{ + TenantID: "00000000-0000-4000-0000-000000000000", + UserID: "000000000000000000000001", + Status: models.MembershipInvitationStatusPending, + ExpiresAt: &[]time.Time{time.Now().Add(14 * (24 * time.Hour))}[0], + }, + nil, + ). + Once() }, expected: Expected{ namespace: nil, @@ -355,7 +352,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "[cloud] fails when the member is duplicated with 'pending' status and expiration date not reached", + description: "[community|enterprise] fails when cannot add the member", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -372,15 +369,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, - }, - { - ID: "000000000000000000000001", - Role: authorizer.RoleAdministrator, - Status: models.MemberStatusPending, - ExpiresAt: time.Now().Add(7 * (24 * time.Hour)), + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, nil). @@ -401,16 +391,20 @@ func TestAddNamespaceMember(t *testing.T) { Once() envMock. On("Get", "SHELLHUB_CLOUD"). - Return("true"). + Return("false"). + Once() + storeMock. + On("WithTransaction", ctx, mock.Anything). + Return(errors.New("error")). Once() }, expected: Expected{ namespace: nil, - err: NewErrNamespaceMemberDuplicated("000000000000000000000001", nil), + err: errors.New("error"), }, }, { - description: "[cloud] succeeds to resend the invite", + description: "[community|enterprise] succeeds", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -427,15 +421,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, - }, - { - ID: "000000000000000000000001", - Role: authorizer.RoleAdministrator, - Status: models.MemberStatusPending, - ExpiresAt: time.Date(2023, 0o1, 0o1, 12, 0o0, 0o0, 0o0, time.UTC), + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, nil). @@ -456,7 +443,7 @@ func TestAddNamespaceMember(t *testing.T) { Once() envMock. On("Get", "SHELLHUB_CLOUD"). - Return("true"). + Return("false"). Once() storeMock. On("WithTransaction", ctx, mock.Anything). @@ -464,26 +451,21 @@ func TestAddNamespaceMember(t *testing.T) { Once() storeMock. On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "00000000-0000-4000-0000-000000000000"). - Return( - &models.Namespace{ - TenantID: "00000000-0000-4000-0000-000000000000", - Name: "namespace", - Owner: "000000000000000000000000", - Members: []models.Member{ - { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, - }, - { - ID: "000000000000000000000001", - Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, - }, + Return(&models.Namespace{ + TenantID: "00000000-0000-4000-0000-000000000000", + Name: "namespace", + Owner: "000000000000000000000000", + Members: []models.Member{ + { + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, + }, + { + ID: "000000000000000000000000", + Role: authorizer.RoleObserver, }, }, - nil, - ). + }, nil). Once() }, expected: Expected{ @@ -493,14 +475,12 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, { - ID: "000000000000000000000001", - Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, + ID: "000000000000000000000000", + Role: authorizer.RoleObserver, }, }, }, @@ -508,7 +488,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "[cloud] succeeds to create the member when not found", + description: "[cloud] succeeds", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -525,9 +505,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, nil). @@ -541,15 +520,18 @@ func TestAddNamespaceMember(t *testing.T) { Once() storeMock. On("UserResolve", ctx, store.UserEmailResolver, "john.doe@test.com"). - Return(nil, store.ErrNoDocuments). + Return(&models.User{ + ID: "000000000000000000000001", + UserData: models.UserData{Username: "john_doe"}, + }, nil). Once() envMock. On("Get", "SHELLHUB_CLOUD"). Return("true"). Once() storeMock. - On("UserInvitationsUpsert", ctx, "john.doe@test.com"). - Return("000000000000000000000001", nil). + On("MembershipInvitationResolve", ctx, "00000000-0000-4000-0000-000000000000", "000000000000000000000001"). + Return(nil, store.ErrNoDocuments). Once() storeMock. On("WithTransaction", ctx, mock.Anything). @@ -557,26 +539,21 @@ func TestAddNamespaceMember(t *testing.T) { Once() storeMock. On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "00000000-0000-4000-0000-000000000000"). - Return( - &models.Namespace{ - TenantID: "00000000-0000-4000-0000-000000000000", - Name: "namespace", - Owner: "000000000000000000000000", - Members: []models.Member{ - { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, - }, - { - ID: "000000000000000000000001", - Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, - }, + Return(&models.Namespace{ + TenantID: "00000000-0000-4000-0000-000000000000", + Name: "namespace", + Owner: "000000000000000000000000", + Members: []models.Member{ + { + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, + }, + { + ID: "000000000000000000000000", + Role: authorizer.RoleObserver, }, }, - nil, - ). + }, nil). Once() }, expected: Expected{ @@ -586,14 +563,12 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, { - ID: "000000000000000000000001", - Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, + ID: "000000000000000000000000", + Role: authorizer.RoleObserver, }, }, }, @@ -601,7 +576,7 @@ func TestAddNamespaceMember(t *testing.T) { }, }, { - description: "fails when cannot add the member", + description: "[cloud] succeeds to resend the invite", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -618,9 +593,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, nil). @@ -641,20 +615,53 @@ func TestAddNamespaceMember(t *testing.T) { Once() envMock. On("Get", "SHELLHUB_CLOUD"). - Return("false"). + Return("true"). + Once() + storeMock. + On("MembershipInvitationResolve", ctx, "00000000-0000-4000-0000-000000000000", "000000000000000000000001"). + Return(&models.MembershipInvitation{ + TenantID: "00000000-0000-4000-0000-000000000000", + UserID: "000000000000000000000001", + Status: models.MembershipInvitationStatusPending, + ExpiresAt: &[]time.Time{time.Date(2023, 0o1, 0o1, 12, 0o0, 0o0, 0o0, time.UTC)}[0], + }, nil). Once() storeMock. On("WithTransaction", ctx, mock.Anything). - Return(errors.New("error")). + Return(nil). + Once() + storeMock. + On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "00000000-0000-4000-0000-000000000000"). + Return(&models.Namespace{ + TenantID: "00000000-0000-4000-0000-000000000000", + Name: "namespace", + Owner: "000000000000000000000000", + Members: []models.Member{ + { + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, + }, + }, + }, nil). Once() }, expected: Expected{ - namespace: nil, - err: errors.New("error"), + namespace: &models.Namespace{ + TenantID: "00000000-0000-4000-0000-000000000000", + Name: "namespace", + Owner: "000000000000000000000000", + Members: []models.Member{ + { + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, + }, + }, + }, + err: nil, }, }, { - description: "succeeds", + description: "[cloud] succeeds to create the user when not found", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", UserID: "000000000000000000000000", @@ -671,9 +678,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, nil). @@ -687,14 +693,23 @@ func TestAddNamespaceMember(t *testing.T) { Once() storeMock. On("UserResolve", ctx, store.UserEmailResolver, "john.doe@test.com"). - Return(&models.User{ - ID: "000000000000000000000001", - UserData: models.UserData{Username: "john_doe"}, - }, nil). + Return(nil, store.ErrNoDocuments). Once() envMock. On("Get", "SHELLHUB_CLOUD"). - Return("false"). + Return("true"). + Once() + storeMock. + On("UserInvitationsUpsert", ctx, "john.doe@test.com"). + Return("000000000000000000000001", nil). + Once() + envMock. + On("Get", "SHELLHUB_CLOUD"). + Return("true"). + Once() + storeMock. + On("MembershipInvitationResolve", ctx, "00000000-0000-4000-0000-000000000000", "000000000000000000000001"). + Return(nil, store.ErrNoDocuments). Once() storeMock. On("WithTransaction", ctx, mock.Anything). @@ -708,14 +723,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, - }, - { - ID: "000000000000000000000000", - Role: authorizer.RoleObserver, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, nil). @@ -728,14 +737,8 @@ func TestAddNamespaceMember(t *testing.T) { Owner: "000000000000000000000000", Members: []models.Member{ { - ID: "000000000000000000000000", - Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, - }, - { - ID: "000000000000000000000000", - Role: authorizer.RoleObserver, - Status: models.MemberStatusAccepted, + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, }, }, }, @@ -771,13 +774,15 @@ func TestService_addMember(t *testing.T) { cases := []struct { description string + namespace *models.Namespace memberID string req *requests.NamespaceAddMember requiredMocks func(context.Context) expected error }{ { - description: "fails cannot add the member", + description: "[community|enterprise] fails when cannot create membership invitation", + namespace: &models.Namespace{TenantID: "00000000-0000-4000-0000-000000000000", Owner: "000000000000000000000000"}, memberID: "000000000000000000000000", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", @@ -792,14 +797,21 @@ func TestService_addMember(t *testing.T) { Return("false"). Once() storeMock. - On("NamespaceCreateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000000", Role: authorizer.RoleObserver, Status: models.MemberStatusAccepted, AddedAt: now, ExpiresAt: time.Time{}}). + On("MembershipInvitationCreate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusAccepted && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt == nil + })). Return(errors.New("error")). Once() }, expected: errors.New("error"), }, { - description: "succeeds", + description: "[community|enterprise] fails when cannot create namespace membership", + namespace: &models.Namespace{TenantID: "00000000-0000-4000-0000-000000000000", Owner: "000000000000000000000000"}, memberID: "000000000000000000000000", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", @@ -814,18 +826,58 @@ func TestService_addMember(t *testing.T) { Return("false"). Once() storeMock. - On("NamespaceCreateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000000", Role: authorizer.RoleObserver, Status: models.MemberStatusAccepted, AddedAt: now, ExpiresAt: time.Time{}}). + On("MembershipInvitationCreate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusAccepted && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt == nil + })). Return(nil). Once() + storeMock. + On("NamespaceCreateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000000", Role: authorizer.RoleObserver, AddedAt: now}). + Return(errors.New("error")). + Once() + }, + expected: errors.New("error"), + }, + { + description: "[community|enterprise] succeeds", + namespace: &models.Namespace{TenantID: "00000000-0000-4000-0000-000000000000", Owner: "000000000000000000000000"}, + memberID: "000000000000000000000000", + req: &requests.NamespaceAddMember{ + FowardedHost: "localhost", + UserID: "000000000000000000000000", + TenantID: "00000000-0000-4000-0000-000000000000", + MemberEmail: "john.doe@test.com", + MemberRole: authorizer.RoleObserver, + }, + requiredMocks: func(ctx context.Context) { envMock. On("Get", "SHELLHUB_CLOUD"). Return("false"). Once() + storeMock. + On("MembershipInvitationCreate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusAccepted && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt == nil + })). + Return(nil). + Once() + storeMock. + On("NamespaceCreateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000000", Role: authorizer.RoleObserver, AddedAt: now}). + Return(nil). + Once() }, expected: nil, }, { - description: "[cloud] fails cannot add the member", + description: "[cloud] fails when cannot create membership invitation", + namespace: &models.Namespace{TenantID: "00000000-0000-4000-0000-000000000000", Owner: "000000000000000000000000"}, memberID: "000000000000000000000000", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", @@ -840,7 +892,13 @@ func TestService_addMember(t *testing.T) { Return("true"). Once() storeMock. - On("NamespaceCreateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000000", Role: authorizer.RoleObserver, Status: models.MemberStatusPending, AddedAt: now, ExpiresAt: now.Add(7 * (24 * time.Hour))}). + On("MembershipInvitationCreate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusPending && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt != nil + })). Return(errors.New("error")). Once() }, @@ -848,6 +906,7 @@ func TestService_addMember(t *testing.T) { }, { description: "[cloud] fails cannot send the invite", + namespace: &models.Namespace{TenantID: "00000000-0000-4000-0000-000000000000", Owner: "000000000000000000000000"}, memberID: "000000000000000000000000", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", @@ -862,13 +921,15 @@ func TestService_addMember(t *testing.T) { Return("true"). Once() storeMock. - On("NamespaceCreateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000000", Role: authorizer.RoleObserver, Status: models.MemberStatusPending, AddedAt: now, ExpiresAt: now.Add(7 * (24 * time.Hour))}). + On("MembershipInvitationCreate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusPending && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt != nil + })). Return(nil). Once() - envMock. - On("Get", "SHELLHUB_CLOUD"). - Return("true"). - Once() clientMock. On("InviteMember", ctx, "00000000-0000-4000-0000-000000000000", "000000000000000000000000", "localhost"). Return(errors.New("error")). @@ -878,6 +939,7 @@ func TestService_addMember(t *testing.T) { }, { description: "[cloud] succeeds", + namespace: &models.Namespace{TenantID: "00000000-0000-4000-0000-000000000000", Owner: "000000000000000000000000"}, memberID: "000000000000000000000000", req: &requests.NamespaceAddMember{ FowardedHost: "localhost", @@ -892,13 +954,15 @@ func TestService_addMember(t *testing.T) { Return("true"). Once() storeMock. - On("NamespaceCreateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000000", Role: authorizer.RoleObserver, Status: models.MemberStatusPending, AddedAt: now, ExpiresAt: now.Add(7 * (24 * time.Hour))}). + On("MembershipInvitationCreate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusPending && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt != nil + })). Return(nil). Once() - envMock. - On("Get", "SHELLHUB_CLOUD"). - Return("true"). - Once() clientMock. On("InviteMember", ctx, "00000000-0000-4000-0000-000000000000", "000000000000000000000000", "localhost"). Return(nil). @@ -915,7 +979,7 @@ func TestService_addMember(t *testing.T) { ctx := context.Background() tc.requiredMocks(ctx) - cb := s.addMember(tc.memberID, tc.req) + cb := s.addMember(tc.namespace, tc.memberID, tc.req) assert.Equal(tt, tc.expected, cb(ctx)) storeMock.AssertExpectations(tt) @@ -924,7 +988,7 @@ func TestService_addMember(t *testing.T) { } } -func TestService_resendMemberInvite(t *testing.T) { +func TestService_resendMembershipInvite(t *testing.T) { envMock = new(envmock.Backend) storeMock := new(storemock.Store) clockMock := new(clockmock.Clock) @@ -937,19 +1001,21 @@ func TestService_resendMemberInvite(t *testing.T) { cases := []struct { description string - member *models.Member + invitation *models.MembershipInvitation req *requests.NamespaceAddMember requiredMocks func(context.Context) expected error }{ { - description: "fails cannot update the member", - member: &models.Member{ - ID: "000000000000000000000000", - AddedAt: now.Add(-7 * (24 * time.Hour)), - ExpiresAt: now.Add(-1 * (24 * time.Hour)), - Role: authorizer.RoleAdministrator, - Status: models.MemberStatusPending, + description: "[cloud] fails when cannot update the invitation", + invitation: &models.MembershipInvitation{ + TenantID: "00000000-0000-4000-0000-000000000000", + UserID: "000000000000000000000000", + Role: authorizer.RoleAdministrator, + Status: models.MembershipInvitationStatusPending, + CreatedAt: now.Add(-7 * (24 * time.Hour)), + ExpiresAt: &[]time.Time{now.Add(-1 * (24 * time.Hour))}[0], + Invitations: 1, }, req: &requests.NamespaceAddMember{ FowardedHost: "localhost", @@ -958,26 +1024,29 @@ func TestService_resendMemberInvite(t *testing.T) { }, requiredMocks: func(ctx context.Context) { storeMock. - On("NamespaceUpdateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ - ID: "000000000000000000000000", - AddedAt: now.Add(-7 * (24 * time.Hour)), - ExpiresAt: now.Add(7 * (24 * time.Hour)), - Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, - }). + On("MembershipInvitationUpdate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusPending && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt != nil && + invitation.Invitations == 2 + })). Return(errors.New("error")). Once() }, expected: errors.New("error"), }, { - description: "fails when cannot send the invite", - member: &models.Member{ - ID: "000000000000000000000000", - AddedAt: now.Add(-7 * (24 * time.Hour)), - ExpiresAt: now.Add(-1 * (24 * time.Hour)), - Role: authorizer.RoleAdministrator, - Status: models.MemberStatusPending, + description: "[cloud] fails when cannot send the invite", + invitation: &models.MembershipInvitation{ + TenantID: "00000000-0000-4000-0000-000000000000", + UserID: "000000000000000000000000", + Role: authorizer.RoleAdministrator, + Status: models.MembershipInvitationStatusPending, + CreatedAt: now.Add(-7 * (24 * time.Hour)), + ExpiresAt: &[]time.Time{now.Add(-1 * (24 * time.Hour))}[0], + Invitations: 1, }, req: &requests.NamespaceAddMember{ FowardedHost: "localhost", @@ -986,13 +1055,14 @@ func TestService_resendMemberInvite(t *testing.T) { }, requiredMocks: func(ctx context.Context) { storeMock. - On("NamespaceUpdateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ - ID: "000000000000000000000000", - AddedAt: now.Add(-7 * (24 * time.Hour)), - ExpiresAt: now.Add(7 * (24 * time.Hour)), - Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, - }). + On("MembershipInvitationUpdate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusPending && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt != nil && + invitation.Invitations == 2 + })). Return(nil). Once() clientMock. @@ -1003,13 +1073,15 @@ func TestService_resendMemberInvite(t *testing.T) { expected: errors.New("error"), }, { - description: "succeeds", - member: &models.Member{ - ID: "000000000000000000000000", - AddedAt: now.Add(-7 * (24 * time.Hour)), - ExpiresAt: now.Add(-1 * (24 * time.Hour)), - Role: authorizer.RoleAdministrator, - Status: models.MemberStatusPending, + description: "[cloud] succeeds", + invitation: &models.MembershipInvitation{ + TenantID: "00000000-0000-4000-0000-000000000000", + UserID: "000000000000000000000000", + Role: authorizer.RoleAdministrator, + Status: models.MembershipInvitationStatusPending, + CreatedAt: now.Add(-7 * (24 * time.Hour)), + ExpiresAt: &[]time.Time{now.Add(-1 * (24 * time.Hour))}[0], + Invitations: 1, }, req: &requests.NamespaceAddMember{ FowardedHost: "localhost", @@ -1018,13 +1090,14 @@ func TestService_resendMemberInvite(t *testing.T) { }, requiredMocks: func(ctx context.Context) { storeMock. - On("NamespaceUpdateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ - ID: "000000000000000000000000", - AddedAt: now.Add(-7 * (24 * time.Hour)), - ExpiresAt: now.Add(7 * (24 * time.Hour)), - Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, - }). + On("MembershipInvitationUpdate", ctx, mock.MatchedBy(func(invitation *models.MembershipInvitation) bool { + return invitation.TenantID == "00000000-0000-4000-0000-000000000000" && + invitation.UserID == "000000000000000000000000" && + invitation.Status == models.MembershipInvitationStatusPending && + invitation.Role == authorizer.RoleObserver && + invitation.ExpiresAt != nil && + invitation.Invitations == 2 + })). Return(nil). Once() clientMock. @@ -1043,7 +1116,7 @@ func TestService_resendMemberInvite(t *testing.T) { ctx := context.Background() tc.requiredMocks(ctx) - cb := s.resendMemberInvite(tc.member, tc.req) + cb := s.resendMembershipInvite(tc.invitation, tc.req) assert.Equal(tt, tc.expected, cb(ctx)) storeMock.AssertExpectations(tt) @@ -1052,9 +1125,12 @@ func TestService_resendMemberInvite(t *testing.T) { } } -func TestUpdateNamespaceMember(t *testing.T) { +func TestService_UpdateNamespaceMember(t *testing.T) { + envMock := new(envmock.Backend) storeMock := new(storemock.Store) + envs.DefaultBackend = envMock + cases := []struct { description string req *requests.NamespaceUpdateMember @@ -1062,7 +1138,7 @@ func TestUpdateNamespaceMember(t *testing.T) { expected error }{ { - description: "fails when the namespace was not found", + description: "[community|enterprise|cloud] fails when the namespace was not found", req: &requests.NamespaceUpdateMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1078,7 +1154,7 @@ func TestUpdateNamespaceMember(t *testing.T) { expected: NewErrNamespaceNotFound("00000000-0000-4000-0000-000000000000", ErrNamespaceNotFound), }, { - description: "fails when the active member was not found", + description: "[community|enterprise|cloud] fails when the active member was not found", req: &requests.NamespaceUpdateMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1103,7 +1179,7 @@ func TestUpdateNamespaceMember(t *testing.T) { expected: NewErrUserNotFound("000000000000000000000000", ErrUserNotFound), }, { - description: "fails when the active member is not on the namespace", + description: "[community|enterprise|cloud] fails when the active member is not on the namespace", req: &requests.NamespaceUpdateMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1131,7 +1207,7 @@ func TestUpdateNamespaceMember(t *testing.T) { expected: NewErrNamespaceMemberNotFound("000000000000000000000000", nil), }, { - description: "fails when the passive member is not on the namespace", + description: "[community|enterprise] fails when the passive member is not on the namespace", req: &requests.NamespaceUpdateMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1164,7 +1240,7 @@ func TestUpdateNamespaceMember(t *testing.T) { expected: NewErrNamespaceMemberNotFound("000000000000000000000001", nil), }, { - description: "fails when the passive role's is owner", + description: "[community|enterprise|cloud] fails when the passive role's is owner", req: &requests.NamespaceUpdateMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1201,7 +1277,7 @@ func TestUpdateNamespaceMember(t *testing.T) { expected: NewErrRoleInvalid(), }, { - description: "fails when the active member's role cannot act over passive member's role", + description: "[community|enterprise|cloud] fails when the active member's role cannot act over passive member's role", req: &requests.NamespaceUpdateMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1238,7 +1314,48 @@ func TestUpdateNamespaceMember(t *testing.T) { expected: NewErrRoleInvalid(), }, { - description: "fails when cannot update the member", + description: "[community|enterprise|cloud] fails when cannot update the member", + req: &requests.NamespaceUpdateMember{ + UserID: "000000000000000000000000", + TenantID: "00000000-0000-4000-0000-000000000000", + MemberID: "000000000000000000000001", + MemberRole: authorizer.RoleAdministrator, + }, + requiredMocks: func(ctx context.Context) { + storeMock. + On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "00000000-0000-4000-0000-000000000000"). + Return(&models.Namespace{ + TenantID: "00000000-0000-4000-0000-000000000000", + Name: "namespace", + Owner: "000000000000000000000000", + Members: []models.Member{ + { + ID: "000000000000000000000000", + Role: authorizer.RoleOwner, + }, + { + ID: "000000000000000000000001", + Role: authorizer.RoleAdministrator, + }, + }, + }, nil). + Once() + storeMock. + On("UserResolve", ctx, store.UserIDResolver, "000000000000000000000000"). + Return(&models.User{ + ID: "000000000000000000000000", + UserData: models.UserData{Username: "jane_doe"}, + }, nil). + Once() + storeMock. + On("NamespaceUpdateMembership", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000001", Role: authorizer.RoleAdministrator}). + Return(errors.New("error")). + Once() + }, + expected: errors.New("error"), + }, + { + description: "[community|enterprise|cloud] succeeds", req: &requests.NamespaceUpdateMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1290,17 +1407,21 @@ func TestUpdateNamespaceMember(t *testing.T) { assert.Equal(t, tc.expected, err) }) } + storeMock.AssertExpectations(t) } -func TestRemoveNamespaceMember(t *testing.T) { +func TestService_RemoveNamespaceMember(t *testing.T) { type Expected struct { namespace *models.Namespace err error } + envMock := new(envmock.Backend) storeMock := new(storemock.Store) + envs.DefaultBackend = envMock + cases := []struct { description string req *requests.NamespaceRemoveMember @@ -1308,7 +1429,7 @@ func TestRemoveNamespaceMember(t *testing.T) { expected Expected }{ { - description: "fails when the namespace was not found", + description: "[community|enterprise|cloud] fails when the namespace was not found", req: &requests.NamespaceRemoveMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1326,7 +1447,7 @@ func TestRemoveNamespaceMember(t *testing.T) { }, }, { - description: "fails when the active member was not found", + description: "[community|enterprise|cloud] fails when the active member was not found", req: &requests.NamespaceRemoveMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1353,7 +1474,7 @@ func TestRemoveNamespaceMember(t *testing.T) { }, }, { - description: "fails when the active member is not on the namespace", + description: "[community|enterprise|cloud] fails when the active member is not on the namespace", req: &requests.NamespaceRemoveMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1383,7 +1504,7 @@ func TestRemoveNamespaceMember(t *testing.T) { }, }, { - description: "fails when the passive member is not on the namespace", + description: "[community|enterprise] fails when the passive member is not on the namespace", req: &requests.NamespaceRemoveMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1418,7 +1539,7 @@ func TestRemoveNamespaceMember(t *testing.T) { }, }, { - description: "fails when the active member's role cannot act over passive member's role", + description: "[community|enterprise|cloud] fails when the active member's role cannot act over passive member's role", req: &requests.NamespaceRemoveMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1457,7 +1578,7 @@ func TestRemoveNamespaceMember(t *testing.T) { }, }, { - description: "fails when cannot remove the member", + description: "[community|enterprise|cloud] fails when cannot remove the member", req: &requests.NamespaceRemoveMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", @@ -1500,7 +1621,7 @@ func TestRemoveNamespaceMember(t *testing.T) { }, }, { - description: "succeeds", + description: "[community|enterprise|cloud] succeeds", req: &requests.NamespaceRemoveMember{ UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", diff --git a/api/services/namespace.go b/api/services/namespace.go index cad8bfe2107..a040de29759 100644 --- a/api/services/namespace.go +++ b/api/services/namespace.go @@ -60,7 +60,6 @@ func (s *service) CreateNamespace(ctx context.Context, req *requests.NamespaceCr { ID: user.ID, Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: clock.Now(), }, }, diff --git a/api/services/namespace_test.go b/api/services/namespace_test.go index 8069959bca0..1af67dae757 100644 --- a/api/services/namespace_test.go +++ b/api/services/namespace_test.go @@ -14,6 +14,8 @@ import ( storecache "github.com/shellhub-io/shellhub/pkg/cache" "github.com/shellhub-io/shellhub/pkg/clock" clockmock "github.com/shellhub-io/shellhub/pkg/clock/mocks" + "github.com/shellhub-io/shellhub/pkg/envs" + envmock "github.com/shellhub-io/shellhub/pkg/envs/mocks" "github.com/shellhub-io/shellhub/pkg/models" "github.com/shellhub-io/shellhub/pkg/uuid" uuidmocks "github.com/shellhub-io/shellhub/pkg/uuid/mocks" @@ -344,8 +346,11 @@ func TestGetNamespace(t *testing.T) { } func TestCreateNamespace(t *testing.T) { + envMock := new(envmock.Backend) storeMock := new(storemock.Store) clockMock := new(clockmock.Clock) + + envs.DefaultBackend = envMock clock.DefaultBackend = clockMock now := time.Now() @@ -530,7 +535,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -607,7 +611,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -631,7 +634,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -706,7 +708,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -730,7 +731,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -802,7 +802,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -826,7 +825,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -898,7 +896,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -922,7 +919,6 @@ func TestCreateNamespace(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, diff --git a/api/services/setup.go b/api/services/setup.go index db3b194628c..2e5b0553905 100644 --- a/api/services/setup.go +++ b/api/services/setup.go @@ -81,7 +81,6 @@ func (s *service) Setup(ctx context.Context, req requests.Setup) error { { ID: insertedID, Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: clock.Now(), }, }, diff --git a/api/services/setup_test.go b/api/services/setup_test.go index 27074e2c0d8..7eea9a43660 100644 --- a/api/services/setup_test.go +++ b/api/services/setup_test.go @@ -191,7 +191,6 @@ func TestSetup(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -282,7 +281,6 @@ func TestSetup(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, @@ -350,7 +348,6 @@ func TestSetup(t *testing.T) { { ID: "000000000000000000000000", Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, AddedAt: now, }, }, diff --git a/api/store/membership-invitations.go b/api/store/membership-invitations.go new file mode 100644 index 00000000000..1f093773b9c --- /dev/null +++ b/api/store/membership-invitations.go @@ -0,0 +1,19 @@ +package store + +import ( + "context" + + "github.com/shellhub-io/shellhub/pkg/models" +) + +type MembershipInvitationsStore interface { + // MembershipInvitationCreate creates a new membership invitation. + MembershipInvitationCreate(ctx context.Context, invitation *models.MembershipInvitation) error + + // MembershipInvitationResolve retrieves the most recent membership invitation for the specified tenant and user. + // It returns the invitation or an error, if any. + MembershipInvitationResolve(ctx context.Context, tenantID, userID string) (*models.MembershipInvitation, error) + + // MembershipInvitationUpdate updates an existing membership invitation. + MembershipInvitationUpdate(ctx context.Context, invitation *models.MembershipInvitation) error +} diff --git a/api/store/mocks/store.go b/api/store/mocks/store.go index 70b61b9d7fd..41e3f81226c 100644 --- a/api/store/mocks/store.go +++ b/api/store/mocks/store.go @@ -552,6 +552,72 @@ func (_m *Store) GetStats(ctx context.Context, tenantID string) (*models.Stats, return r0, r1 } +// MembershipInvitationCreate provides a mock function with given fields: ctx, invitation +func (_m *Store) MembershipInvitationCreate(ctx context.Context, invitation *models.MembershipInvitation) error { + ret := _m.Called(ctx, invitation) + + if len(ret) == 0 { + panic("no return value specified for MembershipInvitationCreate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.MembershipInvitation) error); ok { + r0 = rf(ctx, invitation) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MembershipInvitationResolve provides a mock function with given fields: ctx, tenantID, userID +func (_m *Store) MembershipInvitationResolve(ctx context.Context, tenantID string, userID string) (*models.MembershipInvitation, error) { + ret := _m.Called(ctx, tenantID, userID) + + if len(ret) == 0 { + panic("no return value specified for MembershipInvitationResolve") + } + + var r0 *models.MembershipInvitation + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*models.MembershipInvitation, error)); ok { + return rf(ctx, tenantID, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *models.MembershipInvitation); ok { + r0 = rf(ctx, tenantID, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.MembershipInvitation) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, tenantID, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MembershipInvitationUpdate provides a mock function with given fields: ctx, invitation +func (_m *Store) MembershipInvitationUpdate(ctx context.Context, invitation *models.MembershipInvitation) error { + ret := _m.Called(ctx, invitation) + + if len(ret) == 0 { + panic("no return value specified for MembershipInvitationUpdate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.MembershipInvitation) error); ok { + r0 = rf(ctx, invitation) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NamespaceConflicts provides a mock function with given fields: ctx, target func (_m *Store) NamespaceConflicts(ctx context.Context, target *models.NamespaceConflicts) ([]string, bool, error) { ret := _m.Called(ctx, target) diff --git a/api/store/mongo/fixtures/membership_invitations.json b/api/store/mongo/fixtures/membership_invitations.json new file mode 100644 index 00000000000..1414953a83c --- /dev/null +++ b/api/store/mongo/fixtures/membership_invitations.json @@ -0,0 +1,40 @@ +{ + "membership_invitations": { + "507f1f77bcf86cd799439012": { + "tenant_id": "00000000-0000-4000-0000-000000000000", + "user_id": "6509e169ae6144b2f56bf288", + "invited_by": "507f1f77bcf86cd799439011", + "role": "observer", + "status": "pending", + "created_at": "2023-01-01T12:00:00.000Z", + "updated_at": "2023-01-02T12:00:00.000Z", + "status_updated_at": "2023-01-01T12:00:00.000Z", + "expires_at": "2023-01-08T12:00:00.000Z", + "invitations": 1 + }, + "507f1f77bcf86cd799439013": { + "tenant_id": "00000000-0000-4001-0000-000000000000", + "user_id": "608f32a2c7351f001f6475e0", + "invited_by": "6509e169ae6144b2f56bf288", + "role": "administrator", + "status": "accepted", + "created_at": "2023-01-05T12:00:00.000Z", + "updated_at": "2023-01-06T12:00:00.000Z", + "status_updated_at": "2023-01-06T12:00:00.000Z", + "expires_at": "2023-01-12T12:00:00.000Z", + "invitations": 2 + }, + "507f1f77bcf86cd799439014": { + "tenant_id": "00000000-0000-4000-0000-000000000000", + "user_id": "507f1f77bcf86cd799439011", + "invited_by": "6509e169ae6144b2f56bf288", + "role": "observer", + "status": "pending", + "created_at": "2023-01-07T12:00:00.000Z", + "updated_at": "2023-01-07T12:00:00.000Z", + "status_updated_at": "2023-01-07T12:00:00.000Z", + "expires_at": "2023-01-14T12:00:00.000Z", + "invitations": 1 + } + } +} diff --git a/api/store/mongo/fixtures/namespaces.json b/api/store/mongo/fixtures/namespaces.json index 9c25f107889..81ac29e8156 100644 --- a/api/store/mongo/fixtures/namespaces.json +++ b/api/store/mongo/fixtures/namespaces.json @@ -7,14 +7,12 @@ { "id": "507f1f77bcf86cd799439011", "added_at": "2023-01-01T12:00:00.000Z", - "role": "owner", - "status": "accepted" + "role": "owner" }, { "id": "6509e169ae6144b2f56bf288", "added_at": "2023-01-01T12:00:00.000Z", - "role": "observer", - "status": "pending" + "role": "observer" } ], "name": "namespace-1", @@ -35,14 +33,12 @@ { "id": "6509e169ae6144b2f56bf288", "added_at": "2023-01-01T12:00:00.000Z", - "role": "owner", - "status": "accepted" + "role": "owner" }, { "id": "907f1f77bcf86cd799439022", "added_at": "2023-01-01T12:00:00.000Z", - "role": "operator", - "status": "accepted" + "role": "operator" } ], "name": "namespace-2", @@ -63,8 +59,7 @@ { "id": "657b0e3bff780d625f74e49a", "added_at": "2023-01-01T12:00:00.000Z", - "role": "owner", - "status": "accepted" + "role": "owner" } ], "name": "namespace-3", @@ -85,8 +80,7 @@ { "id": "6577267d8752d05270a4c07d", "added_at": "2023-01-01T12:00:00.000Z", - "role": "owner", - "status": "accepted" + "role": "owner" } ], "name": "namespace-4", diff --git a/api/store/mongo/member.go b/api/store/mongo/member.go index 614f1415ad8..efbc5ada6c8 100644 --- a/api/store/mongo/member.go +++ b/api/store/mongo/member.go @@ -22,11 +22,9 @@ func (s *Store) NamespaceCreateMembership(ctx context.Context, tenantID string, } memberBson := bson.M{ - "id": member.ID, - "added_at": member.AddedAt, - "expires_at": member.ExpiresAt, - "role": member.Role, - "status": member.Status, + "id": member.ID, + "added_at": member.AddedAt, + "role": member.Role, } res, err := s.db. @@ -51,11 +49,9 @@ func (s *Store) NamespaceUpdateMembership(ctx context.Context, tenantID string, filter := bson.M{"tenant_id": tenantID, "members": bson.M{"$elemMatch": bson.M{"id": member.ID}}} memberBson := bson.M{ - "members.$.id": member.ID, - "members.$.added_at": member.AddedAt, - "members.$.expires_at": member.ExpiresAt, - "members.$.role": member.Role, - "members.$.status": member.Status, + "members.$.id": member.ID, + "members.$.added_at": member.AddedAt, + "members.$.role": member.Role, } ns, err := s.db.Collection("namespaces").UpdateOne(ctx, filter, bson.M{"$set": memberBson}) diff --git a/api/store/mongo/member_test.go b/api/store/mongo/member_test.go index 3ed8462bbbc..997ef26080d 100644 --- a/api/store/mongo/member_test.go +++ b/api/store/mongo/member_test.go @@ -30,9 +30,8 @@ func TestNamespaceCreateMembership(t *testing.T) { description: "fails when tenant is not found", tenantID: "nonexistent", member: &models.Member{ - ID: "6509de884238881ac1b2b289", - Role: authorizer.RoleObserver, - Status: models.MemberStatusAccepted, + ID: "6509de884238881ac1b2b289", + Role: authorizer.RoleObserver, }, fixtures: []string{fixtureNamespaces}, expected: Expected{err: store.ErrNoDocuments}, @@ -41,9 +40,8 @@ func TestNamespaceCreateMembership(t *testing.T) { description: "fails when member has already been added", tenantID: "00000000-0000-4000-0000-000000000000", member: &models.Member{ - ID: "6509e169ae6144b2f56bf288", - Role: authorizer.RoleObserver, - Status: models.MemberStatusAccepted, + ID: "6509e169ae6144b2f56bf288", + Role: authorizer.RoleObserver, }, fixtures: []string{fixtureNamespaces}, expected: Expected{err: mongo.ErrNamespaceDuplicatedMember}, @@ -52,9 +50,8 @@ func TestNamespaceCreateMembership(t *testing.T) { description: "succeeds when tenant is found", tenantID: "00000000-0000-4000-0000-000000000000", member: &models.Member{ - ID: "6509de884238881ac1b2b289", - Role: authorizer.RoleObserver, - Status: models.MemberStatusAccepted, + ID: "6509de884238881ac1b2b289", + Role: authorizer.RoleObserver, }, fixtures: []string{fixtureNamespaces}, expected: Expected{err: nil}, @@ -97,9 +94,8 @@ func TestNamespaceUpdateMembership(t *testing.T) { description: "fails when user is not found", tenantID: "00000000-0000-4000-0000-000000000000", member: &models.Member{ - ID: "000000000000000000000000", - Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, + ID: "000000000000000000000000", + Role: authorizer.RoleObserver, }, fixtures: []string{fixtureNamespaces}, expected: Expected{err: mongo.ErrUserNotFound}, @@ -110,7 +106,6 @@ func TestNamespaceUpdateMembership(t *testing.T) { member: &models.Member{ ID: "6509e169ae6144b2f56bf288", Role: authorizer.RoleAdministrator, - Status: models.MemberStatusPending, AddedAt: time.Now(), }, fixtures: []string{fixtureNamespaces}, @@ -138,7 +133,6 @@ func TestNamespaceUpdateMembership(t *testing.T) { require.Equal(t, 2, len(namespace.Members)) require.Equal(t, tc.member.ID, namespace.Members[1].ID) require.Equal(t, tc.member.Role, namespace.Members[1].Role) - require.Equal(t, tc.member.Status, namespace.Members[1].Status) }) } } diff --git a/api/store/mongo/membership-invitation.go b/api/store/mongo/membership-invitation.go new file mode 100644 index 00000000000..2b0d6b11379 --- /dev/null +++ b/api/store/mongo/membership-invitation.go @@ -0,0 +1,146 @@ +package mongo + +import ( + "context" + + "github.com/shellhub-io/shellhub/api/store" + "github.com/shellhub-io/shellhub/pkg/clock" + "github.com/shellhub-io/shellhub/pkg/models" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func (s *Store) MembershipInvitationCreate(ctx context.Context, invitation *models.MembershipInvitation) error { + now := clock.Now() + invitation.CreatedAt = now + invitation.UpdatedAt = now + invitation.StatusUpdatedAt = now + + bsonBytes, err := bson.Marshal(invitation) + if err != nil { + return FromMongoError(err) + } + + doc := make(bson.M) + if err := bson.Unmarshal(bsonBytes, &doc); err != nil { + return FromMongoError(err) + } + + objID := primitive.NewObjectID() + doc["_id"] = objID + doc["user_id"], _ = primitive.ObjectIDFromHex(invitation.UserID) + doc["invited_by"], _ = primitive.ObjectIDFromHex(invitation.InvitedBy) + + if _, err := s.db.Collection("membership_invitations").InsertOne(ctx, doc); err != nil { + return FromMongoError(err) + } + + invitation.ID = objID.Hex() + + return nil +} + +func (s *Store) MembershipInvitationResolve(ctx context.Context, tenantID, userID string) (*models.MembershipInvitation, error) { + userObjID, _ := primitive.ObjectIDFromHex(userID) + + pipeline := []bson.M{ + { + "$match": bson.M{"tenant_id": tenantID, "user_id": userObjID}, + }, + { + "$sort": bson.D{{Key: "_id", Value: -1}}, + }, + { + "$limit": 1, + }, + { + "$lookup": bson.M{ + "from": "namespaces", + "localField": "tenant_id", + "foreignField": "tenant_id", + "as": "namespace", + }, + }, + { + "$lookup": bson.M{ + "from": "users", + "localField": "user_id", + "foreignField": "_id", + "as": "user", + }, + }, + { + "$lookup": bson.M{ + "from": "user_invitations", + "localField": "user_id", + "foreignField": "_id", + "as": "user_invitation", + }, + }, + { + "$addFields": bson.M{ + "namespace_name": bson.M{"$arrayElemAt": bson.A{"$namespace.name", 0}}, + "user_email": bson.M{ + "$ifNull": bson.A{ + bson.M{"$arrayElemAt": bson.A{"$user.email", 0}}, + bson.M{"$arrayElemAt": bson.A{"$user_invitation.email", 0}}, + }, + }, + }, + }, + { + "$project": bson.M{ + "namespace": 0, + "user": 0, + "user_invitation": 0, + }, + }, + } + + cursor, err := s.db.Collection("membership_invitations").Aggregate(ctx, pipeline) + if err != nil { + return nil, FromMongoError(err) + } + defer cursor.Close(ctx) + + if !cursor.Next(ctx) { + return nil, store.ErrNoDocuments + } + + invitation := &models.MembershipInvitation{} + if err := cursor.Decode(invitation); err != nil { + return nil, FromMongoError(err) + } + + return invitation, nil +} + +func (s *Store) MembershipInvitationUpdate(ctx context.Context, invitation *models.MembershipInvitation) error { + invitation.UpdatedAt = clock.Now() + + bsonBytes, err := bson.Marshal(invitation) + if err != nil { + return FromMongoError(err) + } + + doc := make(bson.M) + if err := bson.Unmarshal(bsonBytes, &doc); err != nil { + return FromMongoError(err) + } + + delete(doc, "_id") + doc["user_id"], _ = primitive.ObjectIDFromHex(invitation.UserID) + doc["invited_by"], _ = primitive.ObjectIDFromHex(invitation.InvitedBy) + + objID, _ := primitive.ObjectIDFromHex(invitation.ID) + r, err := s.db.Collection("membership_invitations").UpdateOne(ctx, bson.M{"_id": objID}, bson.M{"$set": doc}) + if err != nil { + return FromMongoError(err) + } + + if r.MatchedCount == 0 { + return store.ErrNoDocuments + } + + return nil +} diff --git a/api/store/mongo/membership-invitation_test.go b/api/store/mongo/membership-invitation_test.go new file mode 100644 index 00000000000..fbcc5261884 --- /dev/null +++ b/api/store/mongo/membership-invitation_test.go @@ -0,0 +1,295 @@ +package mongo_test + +import ( + "context" + "testing" + "time" + + "github.com/shellhub-io/shellhub/api/store" + "github.com/shellhub-io/shellhub/pkg/api/authorizer" + "github.com/shellhub-io/shellhub/pkg/clock" + clockmock "github.com/shellhub-io/shellhub/pkg/clock/mocks" + "github.com/shellhub-io/shellhub/pkg/models" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestStore_MembershipInvitationCreate(t *testing.T) { + mockClock := new(clockmock.Clock) + clock.DefaultBackend = mockClock + + now := time.Now() + mockClock.On("Now").Return(now) + expiresAt := now.Add(7 * 24 * time.Hour) + + cases := []struct { + description string + invitation *models.MembershipInvitation + fixtures []string + expected map[string]any + }{ + { + description: "succeeds creating new invitation", + invitation: &models.MembershipInvitation{ + TenantID: "00000000-0000-4000-0000-000000000000", + UserID: "6509e169ae6144b2f56bf288", + InvitedBy: "507f1f77bcf86cd799439011", + Role: authorizer.RoleObserver, + Status: models.MembershipInvitationStatusPending, + ExpiresAt: &expiresAt, + Invitations: 1, + }, + fixtures: []string{}, + expected: map[string]any{ + "tenant_id": "00000000-0000-4000-0000-000000000000", + "role": "observer", + "status": "pending", + "created_at": primitive.NewDateTimeFromTime(now), + "updated_at": primitive.NewDateTimeFromTime(now), + "status_updated_at": primitive.NewDateTimeFromTime(now), + "invitations": int32(1), + }, + }, + { + description: "succeeds creating invitation with ID", + invitation: &models.MembershipInvitation{ + ID: "507f1f77bcf86cd799439020", + TenantID: "00000000-0000-4001-0000-000000000000", + UserID: "907f1f77bcf86cd799439022", + InvitedBy: "6509e169ae6144b2f56bf288", + Role: authorizer.RoleAdministrator, + Status: models.MembershipInvitationStatusAccepted, + ExpiresAt: &expiresAt, + Invitations: 2, + }, + fixtures: []string{}, + expected: map[string]any{ + "tenant_id": "00000000-0000-4001-0000-000000000000", + "role": "administrator", + "status": "accepted", + "created_at": primitive.NewDateTimeFromTime(now), + "updated_at": primitive.NewDateTimeFromTime(now), + "status_updated_at": primitive.NewDateTimeFromTime(now), + "invitations": int32(2), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.description, func(tt *testing.T) { + ctx := context.Background() + + require.NoError(tt, srv.Apply(tc.fixtures...)) + tt.Cleanup(func() { + require.NoError(tt, srv.Reset()) + }) + + err := s.MembershipInvitationCreate(ctx, tc.invitation) + require.NoError(tt, err) + require.NotEmpty(tt, tc.invitation.ID) + + objID, _ := primitive.ObjectIDFromHex(tc.invitation.ID) + userObjID, _ := primitive.ObjectIDFromHex(tc.invitation.UserID) + invitedByObjID, _ := primitive.ObjectIDFromHex(tc.invitation.InvitedBy) + + tmpInvitation := make(map[string]any) + require.NoError(tt, db.Collection("membership_invitations").FindOne(ctx, bson.M{"_id": objID}).Decode(&tmpInvitation)) + + require.Equal(tt, objID, tmpInvitation["_id"]) + require.Equal(tt, userObjID, tmpInvitation["user_id"]) + require.Equal(tt, invitedByObjID, tmpInvitation["invited_by"]) + + for field, expectedValue := range tc.expected { + require.Equal(tt, expectedValue, tmpInvitation[field]) + } + }) + } +} + +func TestStore_MembershipInvitationResolve(t *testing.T) { + type Expected struct { + invitation *models.MembershipInvitation + err error + } + + cases := []struct { + description string + tenantID string + userID string + fixtures []string + expected Expected + }{ + { + description: "fails when invitation not found", + tenantID: "00000000-0000-4000-0000-000000000000", + userID: "000000000000000000000000", + fixtures: []string{fixtureMembershipInvitations, fixtureNamespaces, fixtureUsers}, + expected: Expected{invitation: nil, err: store.ErrNoDocuments}, + }, + { + description: "succeeds fetching email from users collection", + tenantID: "00000000-0000-4000-0000-000000000000", + userID: "6509e169ae6144b2f56bf288", + fixtures: []string{fixtureMembershipInvitations, fixtureNamespaces, fixtureUsers}, + expected: Expected{ + invitation: &models.MembershipInvitation{ + ID: "507f1f77bcf86cd799439012", + TenantID: "00000000-0000-4000-0000-000000000000", + NamespaceName: "namespace-1", + UserID: "6509e169ae6144b2f56bf288", + UserEmail: "maria.garcia@test.com", + InvitedBy: "507f1f77bcf86cd799439011", + Role: authorizer.RoleObserver, + Status: models.MembershipInvitationStatusPending, + }, + err: nil, + }, + }, + { + description: "succeeds fetching email from user_invitations collection", + tenantID: "00000000-0000-4000-0000-000000000000", + userID: "507f1f77bcf86cd799439011", + fixtures: []string{fixtureMembershipInvitations, fixtureNamespaces, fixtureUserInvitations}, + expected: Expected{ + invitation: &models.MembershipInvitation{ + ID: "507f1f77bcf86cd799439014", + TenantID: "00000000-0000-4000-0000-000000000000", + NamespaceName: "namespace-1", + UserID: "507f1f77bcf86cd799439011", + UserEmail: "jane.doe@test.com", + InvitedBy: "6509e169ae6144b2f56bf288", + Role: authorizer.RoleObserver, + Status: models.MembershipInvitationStatusPending, + }, + err: nil, + }, + }, + { + description: "returns most recent when multiple invitations exist", + tenantID: "00000000-0000-4001-0000-000000000000", + userID: "608f32a2c7351f001f6475e0", + fixtures: []string{fixtureMembershipInvitations, fixtureNamespaces, fixtureUsers}, + expected: Expected{ + invitation: &models.MembershipInvitation{ + ID: "507f1f77bcf86cd799439013", + TenantID: "00000000-0000-4001-0000-000000000000", + NamespaceName: "namespace-2", + UserID: "608f32a2c7351f001f6475e0", + UserEmail: "jane.smith@test.com", + InvitedBy: "6509e169ae6144b2f56bf288", + Role: authorizer.RoleAdministrator, + Status: models.MembershipInvitationStatusAccepted, + }, + err: nil, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.description, func(tt *testing.T) { + ctx := context.Background() + + require.NoError(tt, srv.Apply(tc.fixtures...)) + tt.Cleanup(func() { + require.NoError(tt, srv.Reset()) + }) + + invitation, err := s.MembershipInvitationResolve(ctx, tc.tenantID, tc.userID) + + if tc.expected.err != nil { + require.Equal(tt, tc.expected.err, err) + require.Nil(tt, invitation) + } else { + require.NoError(tt, err) + require.NotNil(tt, invitation) + require.Equal(tt, tc.expected.invitation.ID, invitation.ID) + require.Equal(tt, tc.expected.invitation.TenantID, invitation.TenantID) + require.Equal(tt, tc.expected.invitation.NamespaceName, invitation.NamespaceName) + require.Equal(tt, tc.expected.invitation.UserID, invitation.UserID) + require.Equal(tt, tc.expected.invitation.UserEmail, invitation.UserEmail) + require.Equal(tt, tc.expected.invitation.InvitedBy, invitation.InvitedBy) + require.Equal(tt, tc.expected.invitation.Role, invitation.Role) + require.Equal(tt, tc.expected.invitation.Status, invitation.Status) + } + }) + } +} + +func TestStore_MembershipInvitationUpdate(t *testing.T) { + mockClock := new(clockmock.Clock) + clock.DefaultBackend = mockClock + + now := time.Now() + mockClock.On("Now").Return(now) + + type Expected struct { + err error + } + + cases := []struct { + description string + invitation *models.MembershipInvitation + fixtures []string + expected Expected + }{ + { + description: "fails when invitation not found", + invitation: &models.MembershipInvitation{ + ID: "000000000000000000000000", + TenantID: "00000000-0000-4000-0000-000000000000", + UserID: "6509e169ae6144b2f56bf288", + InvitedBy: "507f1f77bcf86cd799439011", + Role: authorizer.RoleObserver, + Status: models.MembershipInvitationStatusPending, + StatusUpdatedAt: now, + Invitations: 2, + }, + fixtures: []string{fixtureMembershipInvitations}, + expected: Expected{err: store.ErrNoDocuments}, + }, + { + description: "succeeds when invitation found", + invitation: &models.MembershipInvitation{ + ID: "507f1f77bcf86cd799439012", + TenantID: "00000000-0000-4000-0000-000000000000", + UserID: "6509e169ae6144b2f56bf288", + InvitedBy: "507f1f77bcf86cd799439011", + Role: authorizer.RoleAdministrator, + Status: models.MembershipInvitationStatusAccepted, + StatusUpdatedAt: now, + Invitations: 3, + }, + fixtures: []string{fixtureMembershipInvitations}, + expected: Expected{err: nil}, + }, + } + + for _, tc := range cases { + t.Run(tc.description, func(tt *testing.T) { + ctx := context.Background() + + require.NoError(tt, srv.Apply(tc.fixtures...)) + tt.Cleanup(func() { + require.NoError(tt, srv.Reset()) + }) + + err := s.MembershipInvitationUpdate(ctx, tc.invitation) + + if tc.expected.err != nil { + require.Equal(tt, tc.expected.err, err) + } else { + require.NoError(tt, err) + + objID, _ := primitive.ObjectIDFromHex(tc.invitation.ID) + updatedInvitation := &models.MembershipInvitation{} + require.NoError(tt, db.Collection("membership_invitations").FindOne(ctx, bson.M{"_id": objID}).Decode(updatedInvitation)) + + require.Equal(tt, tc.invitation.Role, updatedInvitation.Role) + require.Equal(tt, tc.invitation.Status, updatedInvitation.Status) + require.Equal(tt, tc.invitation.Invitations, updatedInvitation.Invitations) + require.Equal(tt, primitive.NewDateTimeFromTime(now), primitive.NewDateTimeFromTime(updatedInvitation.UpdatedAt)) + } + }) + } +} diff --git a/api/store/mongo/migrations/main.go b/api/store/mongo/migrations/main.go index 3ac04010fb0..cd40a56e77d 100644 --- a/api/store/mongo/migrations/main.go +++ b/api/store/mongo/migrations/main.go @@ -127,6 +127,8 @@ func GenerateMigrations() []migrate.Migration { migration115, migration116, migration117, + migration118, + migration119, } } diff --git a/api/store/mongo/migrations/migration_118.go b/api/store/mongo/migrations/migration_118.go new file mode 100644 index 00000000000..695f554ed9b --- /dev/null +++ b/api/store/mongo/migrations/migration_118.go @@ -0,0 +1,129 @@ +package migrations + +import ( + "context" + + log "github.com/sirupsen/logrus" + migrate "github.com/xakep666/mongo-migrate" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +var migration118 = migrate.Migration{ + Version: 118, + Description: "Migrate member invitations from namespaces members array to membership_invitations collection", + Up: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error { + log.WithFields(log.Fields{ + "component": "migration", + "version": 118, + "action": "Up", + }).Info("Applying migration up") + + session, err := db.Client().StartSession() + if err != nil { + return err + } + defer session.EndSession(ctx) + + _, err = session.WithTransaction(ctx, func(sCtx mongo.SessionContext) (any, error) { + cursor, err := db.Collection("namespaces").Find(sCtx, bson.M{}) + if err != nil { + log.WithError(err).Error("Failed to find namespaces") + + return nil, err + } + + defer cursor.Close(sCtx) + + invitations := make([]any, 0) + namespacesToUpdate := make([]bson.M, 0) + + for cursor.Next(sCtx) { + namespace := make(bson.M) + if err := cursor.Decode(&namespace); err != nil { + log.WithError(err).Error("Failed to decode namespace document") + + return nil, err + } + + if members, ok := namespace["members"].(bson.A); ok { + updatedMembers := make(bson.A, 0) + for _, m := range members { + if member, ok := m.(bson.M); ok { + if member["role"] != "owner" { + invitations = append( + invitations, + bson.M{ + "tenant_id": namespace["tenant_id"], + "user_id": member["id"], + "invited_by": namespace["owner"], + "role": member["role"], + "status": member["status"], + "created_at": member["added_at"], + "updated_at": member["added_at"], + "status_updated_at": member["added_at"], + "expires_at": member["expires_at"], + "invitations": 1, + }, + ) + } + + if member["status"] == "accepted" { + member := bson.M{"id": member["id"], "added_at": member["added_at"], "role": member["role"]} + updatedMembers = append(updatedMembers, member) + } + } + } + + namespace["members"] = updatedMembers + namespacesToUpdate = append(namespacesToUpdate, namespace) + } + } + + if err := cursor.Err(); err != nil { + log.WithError(err).Error("Cursor error while iterating namespaces") + + return nil, err + } + + if len(invitations) > 0 { + if _, err = db.Collection("membership_invitations").InsertMany(sCtx, invitations); err != nil { + log.WithError(err).Error("Failed to insert membership invitations") + + return nil, err + } + + log.WithField("count", len(invitations)).Info("Successfully migrated member invitations to membership_invitations collection") + } else { + log.Info("No member invitations found to migrate") + } + + for _, ns := range namespacesToUpdate { + nsID := ns["_id"] + if _, err = db.Collection("namespaces").ReplaceOne(sCtx, bson.M{"_id": nsID}, ns); err != nil { + log.WithError(err).Error("Failed to update namespace") + + return nil, err + } + } + + if len(namespacesToUpdate) > 0 { + log.WithField("count", len(namespacesToUpdate)).Info("Successfully updated namespaces with cleaned members") + } + + return nil, nil + }) + + return err + }), + + Down: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error { + log.WithFields(log.Fields{ + "component": "migration", + "version": 118, + "action": "Down", + }).Warning("Migration down is not implemented - this migration cannot be reversed safely") + + return nil + }), +} diff --git a/api/store/mongo/migrations/migration_118_test.go b/api/store/mongo/migrations/migration_118_test.go new file mode 100644 index 00000000000..0e2b9da81e1 --- /dev/null +++ b/api/store/mongo/migrations/migration_118_test.go @@ -0,0 +1,169 @@ +package migrations + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + migrate "github.com/xakep666/mongo-migrate" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestMigration118Up(t *testing.T) { + ctx := context.Background() + + cases := []struct { + description string + setup func() error + verify func(tt *testing.T) + }{ + { + description: "succeeds migrating namespace members to membership_invitations collection", + setup: func() error { + ownerID := primitive.NewObjectID() + memberID1 := primitive.NewObjectID() + memberID2 := primitive.NewObjectID() + + namespaces := []bson.M{ + { + "_id": primitive.NewObjectID(), + "name": "test-namespace-1", + "owner": ownerID, + "tenant_id": "tenant-1", + "members": bson.A{ + bson.M{ + "id": ownerID, + "added_at": primitive.NewDateTimeFromTime(primitive.NewObjectID().Timestamp()), + "role": "owner", + "status": "accepted", + "expires_at": nil, + }, + bson.M{ + "id": memberID1, + "added_at": primitive.NewDateTimeFromTime(primitive.NewObjectID().Timestamp()), + "role": "observer", + "status": "pending", + "expires_at": primitive.NewDateTimeFromTime(primitive.NewObjectID().Timestamp().Add(7 * 24 * 60 * 60 * 1000)), + }, + bson.M{ + "id": memberID2, + "added_at": primitive.NewDateTimeFromTime(primitive.NewObjectID().Timestamp()), + "role": "administrator", + "status": "accepted", + "expires_at": nil, + }, + }, + }, + } + + _, err := c.Database("test").Collection("namespaces").InsertMany(ctx, []any{namespaces[0]}) + + return err + }, + verify: func(tt *testing.T) { + cursor, err := c.Database("test").Collection("membership_invitations").Find(ctx, bson.M{}) + require.NoError(tt, err) + + invitations := make([]bson.M, 0) + require.NoError(tt, cursor.All(ctx, &invitations)) + require.Equal(tt, 2, len(invitations)) + + ownerFound := false + for _, invitation := range invitations { + require.NotNil(tt, invitation["_id"]) + require.Equal(tt, "tenant-1", invitation["tenant_id"]) + require.NotNil(tt, invitation["user_id"]) + require.NotNil(tt, invitation["invited_by"]) + require.NotNil(tt, invitation["role"]) + require.NotNil(tt, invitation["status"]) + require.NotNil(tt, invitation["created_at"]) + require.NotNil(tt, invitation["updated_at"]) + require.NotNil(tt, invitation["status_updated_at"]) + require.Equal(tt, int32(1), invitation["invitations"]) + + require.NotEqual(tt, "owner", invitation["role"]) + if invitation["role"] == "owner" { + ownerFound = true + } + } + require.False(tt, ownerFound, "Owner should not have an invitation created") + + namespaceCursor, err := c.Database("test").Collection("namespaces").Find(ctx, bson.M{"tenant_id": "tenant-1"}) + require.NoError(tt, err) + + namespaces := make([]bson.M, 0) + require.NoError(tt, namespaceCursor.All(ctx, &namespaces)) + require.Equal(tt, 1, len(namespaces)) + + namespace := namespaces[0] + members, ok := namespace["members"].(bson.A) + require.True(tt, ok) + require.Equal(tt, 2, len(members)) + + for _, m := range members { + member, ok := m.(bson.M) + require.True(tt, ok) + require.NotNil(tt, member["id"]) + require.NotNil(tt, member["added_at"]) + require.NotNil(tt, member["role"]) + require.Nil(tt, member["status"]) + require.Nil(tt, member["expires_at"]) + } + }, + }, + { + description: "handles namespace with no members gracefully", + setup: func() error { + namespaces := []bson.M{ + { + "_id": primitive.NewObjectID(), + "name": "empty-namespace", + "owner": primitive.NewObjectID(), + "tenant_id": "tenant-empty", + "members": bson.A{}, + }, + } + + _, err := c.Database("test").Collection("namespaces").InsertMany(ctx, []any{namespaces[0]}) + + return err + }, + verify: func(tt *testing.T) { + count, err := c.Database("test").Collection("membership_invitations").CountDocuments(ctx, bson.M{}) + require.NoError(tt, err) + require.Equal(tt, int64(0), count) + + namespaceCount, err := c.Database("test").Collection("namespaces").CountDocuments(ctx, bson.M{"tenant_id": "tenant-empty"}) + require.NoError(tt, err) + require.Equal(tt, int64(1), namespaceCount) + }, + }, + { + description: "handles empty namespaces collection gracefully", + setup: func() error { + return nil + }, + verify: func(tt *testing.T) { + count, err := c.Database("test").Collection("membership_invitations").CountDocuments(ctx, bson.M{}) + require.NoError(tt, err) + require.Equal(tt, int64(0), count) + + namespaceCount, err := c.Database("test").Collection("namespaces").CountDocuments(ctx, bson.M{}) + require.NoError(tt, err) + require.Equal(tt, int64(0), namespaceCount) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.description, func(tt *testing.T) { + tt.Cleanup(func() { require.NoError(tt, srv.Reset()) }) + + require.NoError(tt, tc.setup()) + migrates := migrate.NewMigrate(c.Database("test"), GenerateMigrations()[117]) + require.NoError(tt, migrates.Up(ctx, migrate.AllAvailable)) + tc.verify(tt) + }) + } +} diff --git a/api/store/mongo/migrations/migration_119.go b/api/store/mongo/migrations/migration_119.go new file mode 100644 index 00000000000..8483c2a7b5a --- /dev/null +++ b/api/store/mongo/migrations/migration_119.go @@ -0,0 +1,95 @@ +package migrations + +import ( + "context" + + log "github.com/sirupsen/logrus" + migrate "github.com/xakep666/mongo-migrate" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var migration119 = migrate.Migration{ + Version: 119, + Description: "Create indexes on membership_invitations collection", + Up: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error { + log.WithFields(log.Fields{ + "component": "migration", + "version": 119, + "action": "Up", + }).Info("Applying migration up") + + indexes := []struct { + name string + model mongo.IndexModel + }{ + { + name: "tenant_user_status_pending_unique", + model: mongo.IndexModel{ + Keys: bson.D{ + {Key: "tenant_id", Value: 1}, + {Key: "user_id", Value: 1}, + {Key: "status", Value: 1}, + }, + Options: options.Index(). + SetName("tenant_user_status_pending_unique"). + SetUnique(true). + SetPartialFilterExpression(bson.M{"status": "pending"}), + }, + }, + { + name: "tenant_user_created_at", + model: mongo.IndexModel{ + Keys: bson.D{ + {Key: "tenant_id", Value: 1}, + {Key: "user_id", Value: 1}, + }, + Options: options.Index().SetName("tenant_user_created_at"), + }, + }, + { + name: "user_status", + model: mongo.IndexModel{ + Keys: bson.D{ + {Key: "user_id", Value: 1}, + {Key: "status", Value: 1}, + }, + Options: options.Index().SetName("user_status"), + }, + }, + } + + for _, ix := range indexes { + if _, err := db.Collection("membership_invitations").Indexes().CreateOne(ctx, ix.model); err != nil { + log.WithError(err).WithField("index", ix.name).Error("Failed to create index") + + return err + } + } + + log.Info("Successfully created indexes on membership_invitations collection") + + return nil + }), + Down: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error { + log.WithFields(log.Fields{ + "component": "migration", + "version": 119, + "action": "Down", + }).Info("Applying migration down") + + indexes := []string{"tenant_user_status_pending_unique", "tenant_user_created_at", "user_status"} + for _, ix := range indexes { + if _, err := db.Collection("membership_invitations").Indexes().DropOne(ctx, ix); err != nil { + log.WithError(err).WithField("index", ix).Error("Failed to drop index") + + return err + } + } + + log.Info("Successfully dropped indexes from membership_invitations collection") + + return nil + }), +} diff --git a/api/store/mongo/migrations/migration_72.go b/api/store/mongo/migrations/migration_72.go index e99e3f6a7a7..d888c752c07 100644 --- a/api/store/mongo/migrations/migration_72.go +++ b/api/store/mongo/migrations/migration_72.go @@ -2,7 +2,9 @@ package migrations import ( "context" + "time" + "github.com/shellhub-io/shellhub/pkg/api/authorizer" "github.com/shellhub-io/shellhub/pkg/models" log "github.com/sirupsen/logrus" migrate "github.com/xakep666/mongo-migrate" @@ -10,6 +12,21 @@ import ( "go.mongodb.org/mongo-driver/mongo" ) +// Member struct as it was when migration 72 was created (with Status field) +type memberForMigration72 struct { + ID string `json:"id,omitempty" bson:"id,omitempty"` + AddedAt time.Time `json:"added_at" bson:"added_at"` + Email string `json:"email" bson:"email,omitempty" validate:"email"` + Role authorizer.Role `json:"role" bson:"role" validate:"required,oneof=administrator operator observer"` + Status string `json:"status" bson:"status"` +} + +// Namespace struct for migration 72 with the old Member type +type namespaceForMigration72 struct { + models.Namespace `bson:",inline"` + Members []memberForMigration72 `json:"members" bson:"members"` +} + var migration72 = migrate.Migration{ Version: 72, Description: "Adding the 'members.$.status' attribute to the namespace if it does not already exist.", @@ -39,7 +56,7 @@ var migration72 = migrate.Migration{ updateModels := make([]mongo.WriteModel, 0) for cursor.Next(ctx) { - namespace := new(models.Namespace) + namespace := new(namespaceForMigration72) if err := cursor.Decode(namespace); err != nil { return err } @@ -49,7 +66,7 @@ var migration72 = migrate.Migration{ updateModel := mongo. NewUpdateOneModel(). SetFilter(bson.M{"tenant_id": namespace.TenantID, "members": bson.M{"$elemMatch": bson.M{"id": m.ID}}}). - SetUpdate(bson.M{"$set": bson.M{"members.$.status": models.DeviceStatusAccepted}}) + SetUpdate(bson.M{"$set": bson.M{"members.$.status": "accepted"}}) updateModels = append(updateModels, updateModel) } @@ -90,7 +107,7 @@ var migration72 = migrate.Migration{ updateModels := make([]mongo.WriteModel, 0) for cursor.Next(ctx) { - namespace := new(models.Namespace) + namespace := new(namespaceForMigration72) if err := cursor.Decode(namespace); err != nil { return err } diff --git a/api/store/mongo/namespace.go b/api/store/mongo/namespace.go index d15bd741bf7..8281e6aca81 100644 --- a/api/store/mongo/namespace.go +++ b/api/store/mongo/namespace.go @@ -30,9 +30,6 @@ func (s *Store) NamespaceList(ctx context.Context, opts ...store.QueryOption) ([ "members": bson.M{ "$elemMatch": bson.M{ "id": user.ID, - "status": bson.M{ - "$ne": models.MemberStatusPending, - }, }, }, }, diff --git a/api/store/mongo/namespace_test.go b/api/store/mongo/namespace_test.go index fa91041bbf3..6a5266385d2 100644 --- a/api/store/mongo/namespace_test.go +++ b/api/store/mongo/namespace_test.go @@ -53,13 +53,11 @@ func TestNamespaceList(t *testing.T) { ID: "507f1f77bcf86cd799439011", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, }, { ID: "6509e169ae6144b2f56bf288", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, }, }, MaxDevices: -1, @@ -79,13 +77,11 @@ func TestNamespaceList(t *testing.T) { ID: "6509e169ae6144b2f56bf288", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, }, { ID: "907f1f77bcf86cd799439022", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleOperator, - Status: models.MemberStatusAccepted, }, }, MaxDevices: 10, @@ -105,7 +101,6 @@ func TestNamespaceList(t *testing.T) { ID: "657b0e3bff780d625f74e49a", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, }, }, MaxDevices: 3, @@ -125,7 +120,6 @@ func TestNamespaceList(t *testing.T) { ID: "6577267d8752d05270a4c07d", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, }, }, MaxDevices: -1, @@ -206,14 +200,12 @@ func TestNamespaceResolve(t *testing.T) { ID: "507f1f77bcf86cd799439011", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, Email: "john.doe@test.com", }, { ID: "6509e169ae6144b2f56bf288", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, Email: "maria.garcia@test.com", }, }, @@ -253,14 +245,12 @@ func TestNamespaceResolve(t *testing.T) { ID: "507f1f77bcf86cd799439011", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, Email: "john.doe@test.com", }, { ID: "6509e169ae6144b2f56bf288", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, Email: "maria.garcia@test.com", }, }, @@ -327,13 +317,11 @@ func TestNamespaceGetPreferred(t *testing.T) { ID: "507f1f77bcf86cd799439011", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleOwner, - Status: models.MemberStatusAccepted, }, { ID: "6509e169ae6144b2f56bf288", AddedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), Role: authorizer.RoleObserver, - Status: models.MemberStatusPending, }, }, MaxDevices: -1, diff --git a/api/store/mongo/store_test.go b/api/store/mongo/store_test.go index d4a9cee074a..4920f6a8e74 100644 --- a/api/store/mongo/store_test.go +++ b/api/store/mongo/store_test.go @@ -24,18 +24,19 @@ var ( ) const ( - fixtureAPIKeys = "api-key" // Check "store.mongo.fixtures.api-keys" for fixture info - fixtureDevices = "devices" // Check "store.mongo.fixtures.devices" for fixture info - fixtureSessions = "sessions" // Check "store.mongo.fixtures.sessions" for fixture info - fixtureActiveSessions = "active_sessions" // Check "store.mongo.fixtures.active_sessions" for fixture info - fixtureFirewallRules = "firewall_rules" // Check "store.mongo.fixtures.firewall_rules" for fixture info - fixturePublicKeys = "public_keys" // Check "store.mongo.fixtures.public_keys" for fixture info - fixturePrivateKeys = "private_keys" // Check "store.mongo.fixtures.private_keys" for fixture info - fixtureUsers = "users" // Check "store.mongo.fixtures.users" for fixture iefo - fixtureNamespaces = "namespaces" // Check "store.mongo.fixtures.namespaces" for fixture info - fixtureRecoveryTokens = "recovery_tokens" // Check "store.mongo.fixtures.recovery_tokens" for fixture info - fixtureTags = "tags" // Check "store.mongo.fixtures.tags" for fixture info - fixtureUserInvitations = "user_invitations" // Check "store.mongo.fixtures.user_invitations" for fixture info + fixtureAPIKeys = "api-key" // Check "store.mongo.fixtures.api-keys" for fixture info + fixtureDevices = "devices" // Check "store.mongo.fixtures.devices" for fixture info + fixtureSessions = "sessions" // Check "store.mongo.fixtures.sessions" for fixture info + fixtureActiveSessions = "active_sessions" // Check "store.mongo.fixtures.active_sessions" for fixture info + fixtureFirewallRules = "firewall_rules" // Check "store.mongo.fixtures.firewall_rules" for fixture info + fixturePublicKeys = "public_keys" // Check "store.mongo.fixtures.public_keys" for fixture info + fixturePrivateKeys = "private_keys" // Check "store.mongo.fixtures.private_keys" for fixture info + fixtureUsers = "users" // Check "store.mongo.fixtures.users" for fixture iefo + fixtureNamespaces = "namespaces" // Check "store.mongo.fixtures.namespaces" for fixture info + fixtureRecoveryTokens = "recovery_tokens" // Check "store.mongo.fixtures.recovery_tokens" for fixture info + fixtureTags = "tags" // Check "store.mongo.fixtures.tags" for fixture info + fixtureUserInvitations = "user_invitations" // Check "store.mongo.fixtures.user_invitations" for fixture info + fixtureMembershipInvitations = "membership_invitations" // Check "store.mongo.fixtures.membership_invitations" for fixture info ) func TestMain(m *testing.M) { @@ -53,6 +54,13 @@ func TestMain(m *testing.M) { mongotest.SimpleConvertObjID("user_invitations", "_id"), mongotest.SimpleConvertTime("user_invitations", "created_at"), mongotest.SimpleConvertTime("user_invitations", "updated_at"), + mongotest.SimpleConvertObjID("membership_invitations", "_id"), + mongotest.SimpleConvertObjID("membership_invitations", "user_id"), + mongotest.SimpleConvertObjID("membership_invitations", "invited_by"), + mongotest.SimpleConvertTime("membership_invitations", "created_at"), + mongotest.SimpleConvertTime("membership_invitations", "updated_at"), + mongotest.SimpleConvertTime("membership_invitations", "status_updated_at"), + mongotest.SimpleConvertTime("membership_invitations", "expires_at"), mongotest.SimpleConvertObjID("public_keys", "_id"), mongotest.SimpleConvertBytes("public_keys", "data"), mongotest.SimpleConvertTime("public_keys", "created_at"), diff --git a/api/store/store.go b/api/store/store.go index 8ed8f43d276..f05b8a75a0e 100644 --- a/api/store/store.go +++ b/api/store/store.go @@ -9,6 +9,7 @@ type Store interface { UserInvitationsStore NamespaceStore MemberStore + MembershipInvitationsStore PublicKeyStore PrivateKeyStore StatsStore diff --git a/cli/services/namespaces.go b/cli/services/namespaces.go index 9d61d10625f..0cb52871a45 100644 --- a/cli/services/namespaces.go +++ b/cli/services/namespaces.go @@ -44,7 +44,6 @@ func (s *service) NamespaceCreate(ctx context.Context, input *inputs.NamespaceCr ID: user.ID, Role: authorizer.RoleOwner, AddedAt: clock.Now(), - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -88,7 +87,6 @@ func (s *service) NamespaceAddMember(ctx context.Context, input *inputs.MemberAd ID: user.ID, Role: input.Role, AddedAt: clock.Now(), - Status: models.MemberStatusAccepted, }); err != nil { return nil, ErrFailedNamespaceAddMember } diff --git a/cli/services/namespaces_test.go b/cli/services/namespaces_test.go index e990076d012..e83dbaf72c0 100644 --- a/cli/services/namespaces_test.go +++ b/cli/services/namespaces_test.go @@ -108,7 +108,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -152,7 +151,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -174,7 +172,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -215,7 +212,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -237,7 +233,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -278,7 +273,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -300,7 +294,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -341,7 +334,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -363,7 +355,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -404,7 +395,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -426,7 +416,6 @@ func TestNamespaceCreate(t *testing.T) { ID: "507f191e810c19729de860ea", Role: "owner", AddedAt: now, - Status: models.MemberStatusAccepted, }, }, Settings: &models.NamespaceSettings{ @@ -533,7 +522,6 @@ func TestNamespaceAddMember(t *testing.T) { ID: "507f191e810c19729de860ea", Role: authorizer.RoleObserver, AddedAt: now, - Status: models.MemberStatusAccepted, }).Return(nil).Once() }, expected: Expected{&models.Namespace{ diff --git a/gateway/nginx/conf.d/shellhub.conf b/gateway/nginx/conf.d/shellhub.conf index 0546c063584..65209b061cf 100644 --- a/gateway/nginx/conf.d/shellhub.conf +++ b/gateway/nginx/conf.d/shellhub.conf @@ -400,6 +400,55 @@ server { proxy_set_header X-Request-ID $request_id; } + {{ if $cfg.EnableCloud -}} + location /api/users/invitations { + set $upstream cloud:8080; + limit_req zone=api_limit{{ if $api_burst }} {{ $api_burst }}{{ end }} {{ $api_delay }}; + + auth_request /auth; + auth_request_set $tenant_id $upstream_http_x_tenant_id; + auth_request_set $username $upstream_http_x_username; + auth_request_set $id $upstream_http_x_id; + auth_request_set $api_key $upstream_http_x_api_key; + auth_request_set $role $upstream_http_x_role; + error_page 500 =401 /auth; + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Api-Key $api_key; + proxy_set_header X-ID $id; + proxy_set_header X-Request-ID $request_id; + proxy_set_header X-Role $role; + proxy_set_header X-Tenant-ID $tenant_id; + proxy_set_header X-Username $username; + proxy_pass http://$upstream; + } + + location ~^/api/namespaces/[^/]+/invitations(/.*)?$ { + set $upstream cloud:8080; + limit_req zone=api_limit{{ if $api_burst }} {{ $api_burst }}{{ end }} {{ $api_delay }}; + + auth_request /auth; + auth_request_set $tenant_id $upstream_http_x_tenant_id; + auth_request_set $username $upstream_http_x_username; + auth_request_set $id $upstream_http_x_id; + auth_request_set $api_key $upstream_http_x_api_key; + auth_request_set $role $upstream_http_x_role; + error_page 500 =401 /auth; + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $x_forwarded_port; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + proxy_set_header X-Api-Key $api_key; + proxy_set_header X-ID $id; + proxy_set_header X-Request-ID $request_id; + proxy_set_header X-Role $role; + proxy_set_header X-Tenant-ID $tenant_id; + proxy_set_header X-Username $username; + proxy_pass http://$upstream; + } + {{ end -}} + {{ if $cfg.EnableCloud -}} location /api/announcements { set $upstream cloud:8080; diff --git a/openapi/spec/cloud-openapi.yaml b/openapi/spec/cloud-openapi.yaml index 82e32ffb2ca..65b84cbf8eb 100644 --- a/openapi/spec/cloud-openapi.yaml +++ b/openapi/spec/cloud-openapi.yaml @@ -112,12 +112,20 @@ paths: $ref: paths/api@connector@{uid}.yaml /api/connector/{uid}/info: $ref: paths/api@connector@{uid}@info.yaml - /api/namespaces/{tenant}/members/accept-invite: - $ref: paths/api@namespaces@{tenant}@members@accept-invite.yaml - /api/namespaces/{tenant}/members/invites: - $ref: paths/api@namespaces@{tenant}@members@invites.yaml + /api/namespaces/{tenant}/invitations/links: + $ref: paths/api@namespaces@{tenant}@invitations@links.yaml /api/namespaces/{tenant}/members/{id}/accept-invite: $ref: paths/api@namespaces@{tenant}@members@{id}@accept-invite.yaml + /api/namespaces/{tenant}/invitations/accept: + $ref: paths/api@namespaces@{tenant}@invitations@accept.yaml + /api/namespaces/{tenant}/invitations/decline: + $ref: paths/api@namespaces@{tenant}@invitations@decline.yaml + /api/namespaces/{tenant}/invitations/{user-id}: + $ref: paths/api@namespaces@{tenant}@invitations@{user-id}.yaml + /api/namespaces/{tenant}/invitations: + $ref: paths/api@namespaces@{tenant}@invitations.yaml + /api/users/invitations: + $ref: paths/api@users@invitations.yaml /api/namespaces/{tenant}/support: $ref: paths/api@namespaces@{tenant}@support.yaml /api/web-endpoints: diff --git a/openapi/spec/components/schemas/membershipInvitation.yaml b/openapi/spec/components/schemas/membershipInvitation.yaml new file mode 100644 index 00000000000..376170f9946 --- /dev/null +++ b/openapi/spec/components/schemas/membershipInvitation.yaml @@ -0,0 +1,65 @@ +type: object +description: A membership invitation to a namespace +properties: + namespace: + type: object + description: The namespace associated with this invitation + properties: + tenant_id: + description: The namespace tenant ID + type: string + example: "00000000-0000-4000-0000-000000000000" + name: + description: The namespace name + type: string + example: "my-namespace" + required: + - tenant_id + - name + user: + type: object + description: The invited user + properties: + id: + description: The ID of the invited user + type: string + example: "507f1f77bcf86cd799439011" + email: + description: The email of the invited user + type: string + example: "user@example.com" + required: + - id + - email + invited_by: + description: The ID of the user who sent the invitation + type: string + example: "507f1f77bcf86cd799439012" + created_at: + description: When the invitation was created + type: string + format: date-time + updated_at: + description: When the invitation was last updated + type: string + format: date-time + expires_at: + description: When the invitation expires + type: string + format: date-time + nullable: true + status: + description: The current status of the invitation + type: string + enum: + - pending + - accepted + - rejected + - cancelled + example: pending + status_updated_at: + description: When the status was last updated + type: string + format: date-time + role: + $ref: ./namespaceMemberRole.yaml diff --git a/openapi/spec/openapi.yaml b/openapi/spec/openapi.yaml index 550fed00754..22da1dcd101 100644 --- a/openapi/spec/openapi.yaml +++ b/openapi/spec/openapi.yaml @@ -237,12 +237,22 @@ paths: $ref: paths/api@connector@{uid}.yaml /api/connector/{uid}/info: $ref: paths/api@connector@{uid}@info.yaml - /api/namespaces/{tenant}/members/accept-invite: - $ref: paths/api@namespaces@{tenant}@members@accept-invite.yaml - /api/namespaces/{tenant}/members/invites: - $ref: paths/api@namespaces@{tenant}@members@invites.yaml + # Lookup user status + # TODO: rename this endpoint /api/namespaces/{tenant}/members/{id}/accept-invite: $ref: paths/api@namespaces@{tenant}@members@{id}@accept-invite.yaml + /api/namespaces/{tenant}/invitations/links: + $ref: paths/api@namespaces@{tenant}@invitations@links.yaml + /api/namespaces/{tenant}/invitations/accept: + $ref: paths/api@namespaces@{tenant}@invitations@accept.yaml + /api/namespaces/{tenant}/invitations/decline: + $ref: paths/api@namespaces@{tenant}@invitations@decline.yaml + /api/namespaces/{tenant}/invitations/{user-id}: + $ref: paths/api@namespaces@{tenant}@invitations@{user-id}.yaml + /api/namespaces/{tenant}/invitations: + $ref: paths/api@namespaces@{tenant}@invitations.yaml + /api/users/invitations: + $ref: paths/api@users@invitations.yaml /api/namespaces/{tenant}/support: $ref: paths/api@namespaces@{tenant}@support.yaml /api/web-endpoints: diff --git a/openapi/spec/paths/api@namespaces@{tenant}@invitations.yaml b/openapi/spec/paths/api@namespaces@{tenant}@invitations.yaml new file mode 100644 index 00000000000..b626cc66b62 --- /dev/null +++ b/openapi/spec/paths/api@namespaces@{tenant}@invitations.yaml @@ -0,0 +1,48 @@ +get: + operationId: getNamespaceMembershipInvitationList + summary: Get membership invitations for a namespace + description: | + Returns a paginated list of membership invitations for the specified namespace. + This endpoint allows namespace administrators to view all pending invitations. + tags: + - cloud + - members + - namespaces + security: + - jwt: [] + parameters: + - $ref: ../components/parameters/path/namespaceTenantIDPath.yaml + - name: filter + description: | + Membership invitations filter. + + Filter field receives a base64 encoded JSON object to limit the search. + schema: + type: string + required: false + in: query + - $ref: ../components/parameters/query/pageQuery.yaml + - $ref: ../components/parameters/query/perPageQuery.yaml + responses: + '200': + description: Successfully retrieved namespace membership invitations list. + headers: + X-Total-Count: + description: Total number of membership invitations. + schema: + type: integer + minimum: 0 + content: + application/json: + schema: + type: array + items: + $ref: ../components/schemas/membershipInvitation.yaml + '401': + $ref: ../components/responses/401.yaml + '403': + $ref: ../components/responses/403.yaml + '404': + $ref: ../components/responses/404.yaml + '500': + $ref: ../components/responses/500.yaml diff --git a/openapi/spec/paths/api@namespaces@{tenant}@members@accept-invite.yaml b/openapi/spec/paths/api@namespaces@{tenant}@invitations@accept.yaml similarity index 61% rename from openapi/spec/paths/api@namespaces@{tenant}@members@accept-invite.yaml rename to openapi/spec/paths/api@namespaces@{tenant}@invitations@accept.yaml index 72bad69d4de..abb9c12a51d 100644 --- a/openapi/spec/paths/api@namespaces@{tenant}@members@accept-invite.yaml +++ b/openapi/spec/paths/api@namespaces@{tenant}@invitations@accept.yaml @@ -2,7 +2,7 @@ patch: operationId: acceptInvite summary: Accept a membership invite description: | - This route is intended to be accessed directly through the link sent in the invitation email. + Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. tags: - cloud @@ -13,18 +13,6 @@ patch: - jwt: [] parameters: - $ref: ../components/parameters/path/namespaceTenantIDPath.yaml - requestBody: - content: - application/json: - schema: - type: object - properties: - sig: - description: The unique key included in the email link. - type: string - example: b25e93bc-22ac-4f02-901a-52af9f358a5d - required: - - sig responses: '200': description: Invitation successfully accepted diff --git a/openapi/spec/paths/api@namespaces@{tenant}@invitations@decline.yaml b/openapi/spec/paths/api@namespaces@{tenant}@invitations@decline.yaml new file mode 100644 index 00000000000..bbf7a524dda --- /dev/null +++ b/openapi/spec/paths/api@namespaces@{tenant}@invitations@decline.yaml @@ -0,0 +1,29 @@ +patch: + operationId: declineInvite + summary: Decline a membership invite + description: | + Declines a pending membership invitation for the authenticated user. + The user must be logged into the account that was invited. + The invitation status will be updated to "rejected". + tags: + - cloud + - members + - namespaces + - users + security: + - jwt: [] + parameters: + - $ref: ../components/parameters/path/namespaceTenantIDPath.yaml + responses: + '200': + description: Invitation successfully declined + '400': + $ref: ../components/responses/400.yaml + '401': + $ref: ../components/responses/401.yaml + '403': + $ref: ../components/responses/403.yaml + '404': + $ref: ../components/responses/404.yaml + '500': + $ref: ../components/responses/500.yaml diff --git a/openapi/spec/paths/api@namespaces@{tenant}@members@invites.yaml b/openapi/spec/paths/api@namespaces@{tenant}@invitations@links.yaml similarity index 100% rename from openapi/spec/paths/api@namespaces@{tenant}@members@invites.yaml rename to openapi/spec/paths/api@namespaces@{tenant}@invitations@links.yaml diff --git a/openapi/spec/paths/api@namespaces@{tenant}@invitations@{user-id}.yaml b/openapi/spec/paths/api@namespaces@{tenant}@invitations@{user-id}.yaml new file mode 100644 index 00000000000..f1ba34f343b --- /dev/null +++ b/openapi/spec/paths/api@namespaces@{tenant}@invitations@{user-id}.yaml @@ -0,0 +1,77 @@ +patch: + operationId: updateMembershipInvitation + summary: Update a pending membership invitation + description: | + Allows namespace administrators to update a pending membership invitation. + Currently supports updating the role assigned to the invitation. + The active user must have authority over the role being assigned. + tags: + - cloud + - members + - namespaces + security: + - jwt: [] + parameters: + - $ref: ../components/parameters/path/namespaceTenantIDPath.yaml + - name: user-id + description: The ID of the invited user + schema: + type: string + required: true + in: path + requestBody: + content: + application/json: + schema: + type: object + properties: + role: + $ref: ../components/schemas/namespaceMemberRole.yaml + responses: + '200': + description: Invitation successfully updated + '400': + $ref: ../components/responses/400.yaml + '401': + $ref: ../components/responses/401.yaml + '403': + $ref: ../components/responses/403.yaml + '404': + $ref: ../components/responses/404.yaml + '500': + $ref: ../components/responses/500.yaml + +delete: + operationId: cancelMembershipInvitation + summary: Cancel a pending membership invitation + description: | + Allows namespace administrators to cancel a pending membership invitation. + The invitation status will be updated to "cancelled". + The active user must have authority over the role of the invitation being cancelled. + tags: + - cloud + - members + - namespaces + security: + - jwt: [] + parameters: + - $ref: ../components/parameters/path/namespaceTenantIDPath.yaml + - name: user-id + description: The ID of the invited user + schema: + type: string + required: true + in: path + responses: + '200': + description: Invitation successfully cancelled + '400': + $ref: ../components/responses/400.yaml + '401': + $ref: ../components/responses/401.yaml + '403': + $ref: ../components/responses/403.yaml + '404': + $ref: ../components/responses/404.yaml + '500': + $ref: ../components/responses/500.yaml diff --git a/openapi/spec/paths/api@users@invitations.yaml b/openapi/spec/paths/api@users@invitations.yaml new file mode 100644 index 00000000000..62cbe7e7de2 --- /dev/null +++ b/openapi/spec/paths/api@users@invitations.yaml @@ -0,0 +1,43 @@ +get: + operationId: getMembershipInvitationList + summary: Get membership invitations for the authenticated user + description: | + Returns a paginated list of membership invitations for the authenticated user. + This endpoint allows users to view all namespace invitations they have received. + tags: + - cloud + - members + - namespaces + security: + - jwt: [] + parameters: + - name: filter + description: | + Membership invitations filter. + + Filter field receives a base64 encoded JSON object to limit the search. + schema: + type: string + required: false + in: query + - $ref: ../components/parameters/query/pageQuery.yaml + - $ref: ../components/parameters/query/perPageQuery.yaml + responses: + '200': + description: Successfully retrieved membership invitations list. + headers: + X-Total-Count: + description: Total number of membership invitations. + schema: + type: integer + minimum: 0 + content: + application/json: + schema: + type: array + items: + $ref: ../components/schemas/membershipInvitation.yaml + '401': + $ref: ../components/responses/401.yaml + '500': + $ref: ../components/responses/500.yaml diff --git a/pkg/models/member.go b/pkg/models/member.go index 758f8e991b2..77bea7908b0 100644 --- a/pkg/models/member.go +++ b/pkg/models/member.go @@ -6,22 +6,9 @@ import ( "github.com/shellhub-io/shellhub/pkg/api/authorizer" ) -type MemberStatus string - -const ( - MemberStatusPending MemberStatus = "pending" - MemberStatusAccepted MemberStatus = "accepted" -) - type Member struct { - ID string `json:"id,omitempty" bson:"id,omitempty"` - AddedAt time.Time `json:"added_at" bson:"added_at"` - - // ExpiresAt specifies the expiration date of the invite. This attribute is only applicable in *Cloud* instances, - // and it is ignored for members whose status is not 'pending'. - ExpiresAt time.Time `json:"expires_at" bson:"expires_at"` - - Email string `json:"email" bson:"email,omitempty" validate:"email"` - Role authorizer.Role `json:"role" bson:"role" validate:"required,oneof=administrator operator observer"` - Status MemberStatus `json:"status" bson:"status"` + ID string `json:"id,omitempty" bson:"id,omitempty"` + AddedAt time.Time `json:"added_at" bson:"added_at"` + Email string `json:"email" bson:"email,omitempty" validate:"email"` + Role authorizer.Role `json:"role" bson:"role" validate:"required,oneof=administrator operator observer"` } diff --git a/pkg/models/membership-invitation.go b/pkg/models/membership-invitation.go new file mode 100644 index 00000000000..f5cdfb8e621 --- /dev/null +++ b/pkg/models/membership-invitation.go @@ -0,0 +1,44 @@ +package models + +import ( + "time" + + "github.com/shellhub-io/shellhub/pkg/api/authorizer" + "github.com/shellhub-io/shellhub/pkg/clock" +) + +type MembershipInvitationStatus string + +const ( + MembershipInvitationStatusPending MembershipInvitationStatus = "pending" + MembershipInvitationStatusAccepted MembershipInvitationStatus = "accepted" + MembershipInvitationStatusRejected MembershipInvitationStatus = "rejected" + MembershipInvitationStatusCancelled MembershipInvitationStatus = "cancelled" +) + +type MembershipInvitation struct { + ID string `json:"-" bson:"_id"` + TenantID string `json:"-" bson:"tenant_id"` + UserID string `json:"-" bson:"user_id"` + InvitedBy string `json:"invited_by" bson:"invited_by"` + CreatedAt time.Time `json:"created_at" bson:"created_at"` + UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` + ExpiresAt *time.Time `json:"expires_at" bson:"expires_at"` + Status MembershipInvitationStatus `json:"status" bson:"status"` + StatusUpdatedAt time.Time `json:"status_updated_at" bson:"status_updated_at"` + Role authorizer.Role `json:"role" bson:"role"` + Invitations int `json:"-" bson:"invitations"` + + // NamespaceName isn't saved on the database + NamespaceName string `json:"-" bson:"namespace_name,omitempty"` + // UserEmail isn't saved on the database + UserEmail string `json:"-" bson:"user_email,omitempty"` +} + +func (m MembershipInvitation) IsExpired() bool { + return m.ExpiresAt != nil && m.ExpiresAt.Before(clock.Now()) +} + +func (m MembershipInvitation) IsPending() bool { + return m.Status == MembershipInvitationStatusPending +} diff --git a/ui/src/api/client/.openapi-generator-ignore b/ui/src/api/client/.openapi-generator-ignore new file mode 100644 index 00000000000..7484ee590a3 --- /dev/null +++ b/ui/src/api/client/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/ui/src/api/client/.openapi-generator/FILES b/ui/src/api/client/.openapi-generator/FILES index 5dbdabfbf61..eb060838798 100644 --- a/ui/src/api/client/.openapi-generator/FILES +++ b/ui/src/api/client/.openapi-generator/FILES @@ -4,7 +4,6 @@ api.ts base.ts common.ts configuration.ts -docs/AcceptInviteRequest.md docs/AddNamespaceMemberRequest.md docs/AdminApi.md docs/AdminResetUserPassword200Response.md @@ -120,6 +119,10 @@ docs/LoginAdminRequest.md docs/LoginRequest.md docs/LookupUserStatus200Response.md docs/MembersApi.md +docs/MembershipInvitation.md +docs/MembershipInvitationNamespace.md +docs/MembershipInvitationStatus.md +docs/MembershipInvitationUser.md docs/MfaApi.md docs/MfaAuth.md docs/MfaDisable.md diff --git a/ui/src/api/client/api.ts b/ui/src/api/client/api.ts index d5628618359..9e0697381e4 100644 --- a/ui/src/api/client/api.ts +++ b/ui/src/api/client/api.ts @@ -23,12 +23,6 @@ import type { RequestArgs } from './base'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base'; -export interface AcceptInviteRequest { - /** - * The unique key included in the email link. - */ - 'sig': string; -} export interface AddNamespaceMemberRequest { /** * The email of the member. @@ -1652,6 +1646,77 @@ export interface LoginRequest { export interface LookupUserStatus200Response { 'status'?: string; } +/** + * A membership invitation to a namespace + */ +export interface MembershipInvitation { + 'namespace'?: MembershipInvitationNamespace; + 'user'?: MembershipInvitationUser; + /** + * The ID of the user who sent the invitation + */ + 'invited_by'?: string; + /** + * When the invitation was created + */ + 'created_at'?: string; + /** + * When the invitation was last updated + */ + 'updated_at'?: string; + /** + * When the invitation expires + */ + 'expires_at'?: string | null; + 'status'?: MembershipInvitationStatus; + /** + * When the status was last updated + */ + 'status_updated_at'?: string; + 'role'?: NamespaceMemberRole; +} + + +/** + * The namespace associated with this invitation + */ +export interface MembershipInvitationNamespace { + /** + * The namespace tenant ID + */ + 'tenant_id': string; + /** + * The namespace name + */ + 'name': string; +} +/** + * The current status of the invitation + */ + +export const MembershipInvitationStatus = { + Pending: 'pending', + Accepted: 'accepted', + Rejected: 'rejected', + Cancelled: 'cancelled' +} as const; + +export type MembershipInvitationStatus = typeof MembershipInvitationStatus[keyof typeof MembershipInvitationStatus]; + + +/** + * The invited user + */ +export interface MembershipInvitationUser { + /** + * The ID of the invited user + */ + 'id': string; + /** + * The email of the invited user + */ + 'email': string; +} export interface MfaAuth { /** * The `X-MFA-Token` header returned by the authUser endpoint. @@ -7232,17 +7297,16 @@ export class BillingApi extends BaseAPI { export const CloudApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - acceptInvite: async (tenant: string, acceptInviteRequest?: AcceptInviteRequest, options: RawAxiosRequestConfig = {}): Promise => { + acceptInvite: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined assertParamExists('acceptInvite', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}/members/accept-invite` + const localVarPath = `/api/namespaces/{tenant}/invitations/accept` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -7261,12 +7325,9 @@ export const CloudApiAxiosParamCreator = function (configuration?: Configuration - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(acceptInviteRequest, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -7348,6 +7409,48 @@ export const CloudApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelMembershipInvitation: async (tenant: string, userId: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('cancelMembershipInvitation', 'tenant', tenant) + // verify required parameter 'userId' is not null or undefined + assertParamExists('cancelMembershipInvitation', 'userId', userId) + const localVarPath = `/api/namespaces/{tenant}/invitations/{user-id}` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))) + .replace(`{${"user-id"}}`, encodeURIComponent(String(userId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Choice devices when device\'s limit is rechead. * @summary Choice devices @@ -7946,6 +8049,44 @@ export const CloudApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + declineInvite: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('declineInvite', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations/decline` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Delete an announcement. * @summary Delete an announcement @@ -8359,7 +8500,7 @@ export const CloudApiAxiosParamCreator = function (configuration?: Configuration generateInvitationLink: async (tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined assertParamExists('generateInvitationLink', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}/members/invites` + const localVarPath = `/api/namespaces/{tenant}/invitations/links` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -8618,6 +8759,108 @@ export const CloudApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMembershipInvitationList: async (filter?: string, page?: number, perPage?: number, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/users/invitations`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (filter !== undefined) { + localVarQueryParameter['filter'] = filter; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (perPage !== undefined) { + localVarQueryParameter['per_page'] = perPage; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getNamespaceMembershipInvitationList: async (tenant: string, filter?: string, page?: number, perPage?: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('getNamespaceMembershipInvitationList', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (filter !== undefined) { + localVarQueryParameter['filter'] = filter; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (perPage !== undefined) { + localVarQueryParameter['per_page'] = perPage; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -9422,6 +9665,52 @@ export const CloudApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateMembershipInvitation: async (tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('updateMembershipInvitation', 'tenant', tenant) + // verify required parameter 'userId' is not null or undefined + assertParamExists('updateMembershipInvitation', 'userId', userId) + const localVarPath = `/api/namespaces/{tenant}/invitations/{user-id}` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))) + .replace(`{${"user-id"}}`, encodeURIComponent(String(userId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateNamespaceMemberRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Update user password from a recovery token got from email. * @summary Update user password @@ -9470,15 +9759,14 @@ export const CloudApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = CloudApiAxiosParamCreator(configuration) return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.acceptInvite(tenant, acceptInviteRequest, options); + async acceptInvite(tenant: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.acceptInvite(tenant, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['CloudApi.acceptInvite']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -9510,16 +9798,30 @@ export const CloudApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Choice devices when device\'s limit is rechead. - * @summary Choice devices - * @param {ChoiceDevicesRequest} [choiceDevicesRequest] + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async choiceDevices(choiceDevicesRequest?: ChoiceDevicesRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.choiceDevices(choiceDevicesRequest, options); + async cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancelMembershipInvitation(tenant, userId, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['CloudApi.choiceDevices']?.[localVarOperationServerIndex]?.url; + const localVarOperationServerBasePath = operationServerMap['CloudApi.cancelMembershipInvitation']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Choice devices when device\'s limit is rechead. + * @summary Choice devices + * @param {ChoiceDevicesRequest} [choiceDevicesRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async choiceDevices(choiceDevicesRequest?: ChoiceDevicesRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.choiceDevices(choiceDevicesRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['CloudApi.choiceDevices']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** @@ -9695,6 +9997,19 @@ export const CloudApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['CloudApi.createWebEndpoint']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async declineInvite(tenant: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.declineInvite(tenant, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['CloudApi.declineInvite']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Delete an announcement. * @summary Delete an announcement @@ -9916,6 +10231,37 @@ export const CloudApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['CloudApi.getFirewallRules']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMembershipInvitationList(filter, page, perPage, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['CloudApi.getMembershipInvitationList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['CloudApi.getNamespaceMembershipInvitationList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Get a namespace support identifier. * @summary Get a namespace support identifier. @@ -10177,6 +10523,21 @@ export const CloudApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['CloudApi.updateFirewallRule']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['CloudApi.updateMembershipInvitation']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Update user password from a recovery token got from email. * @summary Update user password @@ -10201,15 +10562,14 @@ export const CloudApiFactory = function (configuration?: Configuration, basePath const localVarFp = CloudApiFp(configuration) return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.acceptInvite(tenant, acceptInviteRequest, options).then((request) => request(axios, basePath)); + acceptInvite(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.acceptInvite(tenant, options).then((request) => request(axios, basePath)); }, /** * Attachs a payment method to a customer. @@ -10231,6 +10591,17 @@ export const CloudApiFactory = function (configuration?: Configuration, basePath authMFA(mfaAuth?: MfaAuth, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.authMFA(mfaAuth, options).then((request) => request(axios, basePath)); }, + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.cancelMembershipInvitation(tenant, userId, options).then((request) => request(axios, basePath)); + }, /** * Choice devices when device\'s limit is rechead. * @summary Choice devices @@ -10375,6 +10746,16 @@ export const CloudApiFactory = function (configuration?: Configuration, basePath createWebEndpoint(createWebEndpointRequest: CreateWebEndpointRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.createWebEndpoint(createWebEndpointRequest, options).then((request) => request(axios, basePath)); }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + declineInvite(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.declineInvite(tenant, options).then((request) => request(axios, basePath)); + }, /** * Delete an announcement. * @summary Delete an announcement @@ -10545,6 +10926,31 @@ export const CloudApiFactory = function (configuration?: Configuration, basePath getFirewallRules(page?: number, perPage?: number, options?: RawAxiosRequestConfig): AxiosPromise> { return localVarFp.getFirewallRules(page, perPage, options).then((request) => request(axios, basePath)); }, + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.getMembershipInvitationList(filter, page, perPage, options).then((request) => request(axios, basePath)); + }, + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options).then((request) => request(axios, basePath)); + }, /** * Get a namespace support identifier. * @summary Get a namespace support identifier. @@ -10749,6 +11155,18 @@ export const CloudApiFactory = function (configuration?: Configuration, basePath updateFirewallRule(id: string, firewallRulesRequest?: FirewallRulesRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.updateFirewallRule(id, firewallRulesRequest, options).then((request) => request(axios, basePath)); }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options).then((request) => request(axios, basePath)); + }, /** * Update user password from a recovery token got from email. * @summary Update user password @@ -10768,15 +11186,14 @@ export const CloudApiFactory = function (configuration?: Configuration, basePath */ export class CloudApi extends BaseAPI { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig) { - return CloudApiFp(this.configuration).acceptInvite(tenant, acceptInviteRequest, options).then((request) => request(this.axios, this.basePath)); + public acceptInvite(tenant: string, options?: RawAxiosRequestConfig) { + return CloudApiFp(this.configuration).acceptInvite(tenant, options).then((request) => request(this.axios, this.basePath)); } /** @@ -10801,6 +11218,18 @@ export class CloudApi extends BaseAPI { return CloudApiFp(this.configuration).authMFA(mfaAuth, options).then((request) => request(this.axios, this.basePath)); } + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig) { + return CloudApiFp(this.configuration).cancelMembershipInvitation(tenant, userId, options).then((request) => request(this.axios, this.basePath)); + } + /** * Choice devices when device\'s limit is rechead. * @summary Choice devices @@ -10959,6 +11388,17 @@ export class CloudApi extends BaseAPI { return CloudApiFp(this.configuration).createWebEndpoint(createWebEndpointRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public declineInvite(tenant: string, options?: RawAxiosRequestConfig) { + return CloudApiFp(this.configuration).declineInvite(tenant, options).then((request) => request(this.axios, this.basePath)); + } + /** * Delete an announcement. * @summary Delete an announcement @@ -11146,6 +11586,33 @@ export class CloudApi extends BaseAPI { return CloudApiFp(this.configuration).getFirewallRules(page, perPage, options).then((request) => request(this.axios, this.basePath)); } + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig) { + return CloudApiFp(this.configuration).getMembershipInvitationList(filter, page, perPage, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig) { + return CloudApiFp(this.configuration).getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get a namespace support identifier. * @summary Get a namespace support identifier. @@ -11369,6 +11836,19 @@ export class CloudApi extends BaseAPI { return CloudApiFp(this.configuration).updateFirewallRule(id, firewallRulesRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig) { + return CloudApiFp(this.configuration).updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * Update user password from a recovery token got from email. * @summary Update user password @@ -21754,17 +22234,16 @@ export class LicenseApi extends BaseAPI { export const MembersApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - acceptInvite: async (tenant: string, acceptInviteRequest?: AcceptInviteRequest, options: RawAxiosRequestConfig = {}): Promise => { + acceptInvite: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined assertParamExists('acceptInvite', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}/members/accept-invite` + const localVarPath = `/api/namespaces/{tenant}/invitations/accept` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -21783,12 +22262,9 @@ export const MembersApiAxiosParamCreator = function (configuration?: Configurati - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(acceptInviteRequest, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -21838,18 +22314,21 @@ export const MembersApiAxiosParamCreator = function (configuration?: Configurati }; }, /** - * Generates a unique invitation link to invite a member to a namespace using their email. Each invitation link is unique and tied to the provided email. Upon accepting the invitation, the user\'s status will automatically be set to `accepted`. If the user associated with the email does not exist, the invitation link will redirect them to the signup page. The invitation remains valid for **7 days**. - * @summary Generate an invitation link for a namespace member + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation * @param {string} tenant Namespace\'s tenant ID - * @param {AddNamespaceMemberRequest} [addNamespaceMemberRequest] + * @param {string} userId The ID of the invited user * @param {*} [options] Override http request option. * @throws {RequiredError} */ - generateInvitationLink: async (tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options: RawAxiosRequestConfig = {}): Promise => { + cancelMembershipInvitation: async (tenant: string, userId: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined - assertParamExists('generateInvitationLink', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}/members/invites` - .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); + assertParamExists('cancelMembershipInvitation', 'tenant', tenant) + // verify required parameter 'userId' is not null or undefined + assertParamExists('cancelMembershipInvitation', 'userId', userId) + const localVarPath = `/api/namespaces/{tenant}/invitations/{user-id}` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))) + .replace(`{${"user-id"}}`, encodeURIComponent(String(userId))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -21857,7 +22336,7 @@ export const MembersApiAxiosParamCreator = function (configuration?: Configurati baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -21867,12 +22346,9 @@ export const MembersApiAxiosParamCreator = function (configuration?: Configurati - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(addNamespaceMemberRequest, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -21880,16 +22356,16 @@ export const MembersApiAxiosParamCreator = function (configuration?: Configurati }; }, /** - * Allows the authenticated user to leave the specified namespace. Owners cannot leave a namespace; they must delete it instead. If the user attempts to leave their current authenticated namespace, the response will provide a new token that excludes this namespace. - * @summary Leave Namespace + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite * @param {string} tenant Namespace\'s tenant ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - leaveNamespace: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { + declineInvite: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined - assertParamExists('leaveNamespace', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}/members` + assertParamExists('declineInvite', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations/decline` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -21898,13 +22374,10 @@ export const MembersApiAxiosParamCreator = function (configuration?: Configurati baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - // authentication api-key required - await setApiKeyToObject(localVarHeaderParameter, "X-API-KEY", configuration) - // authentication jwt required // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) @@ -21921,24 +22394,18 @@ export const MembersApiAxiosParamCreator = function (configuration?: Configurati }; }, /** - * Clients may need to check a user\'s status before deciding whether to redirect to the accept-invite workflow or to the signup process. It is intended for use exclusively by clients in the `invite-member` pipeline. - * @summary Lookup User\'s Status - * @param {string} tenant The tenant ID of the namespace. - * @param {string} id The user\'s ID. - * @param {string} sig The signature included in the email. This is used instead of the user\'s token to authenticate the request. + * Generates a unique invitation link to invite a member to a namespace using their email. Each invitation link is unique and tied to the provided email. Upon accepting the invitation, the user\'s status will automatically be set to `accepted`. If the user associated with the email does not exist, the invitation link will redirect them to the signup page. The invitation remains valid for **7 days**. + * @summary Generate an invitation link for a namespace member + * @param {string} tenant Namespace\'s tenant ID + * @param {AddNamespaceMemberRequest} [addNamespaceMemberRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - lookupUserStatus: async (tenant: string, id: string, sig: string, options: RawAxiosRequestConfig = {}): Promise => { + generateInvitationLink: async (tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined - assertParamExists('lookupUserStatus', 'tenant', tenant) - // verify required parameter 'id' is not null or undefined - assertParamExists('lookupUserStatus', 'id', id) - // verify required parameter 'sig' is not null or undefined - assertParamExists('lookupUserStatus', 'sig', sig) - const localVarPath = `/api/namespaces/{tenant}/members/{id}/accept-invite` - .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))) - .replace(`{${"id"}}`, encodeURIComponent(String(id))); + assertParamExists('generateInvitationLink', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations/links` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -21946,74 +22413,368 @@ export const MembersApiAxiosParamCreator = function (configuration?: Configurati baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - if (sig !== undefined) { - localVarQueryParameter['sig'] = sig; - } + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + localVarHeaderParameter['Content-Type'] = 'application/json'; + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(addNamespaceMemberRequest, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, - } -}; - -/** - * MembersApi - functional programming interface - */ -export const MembersApiFp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = MembersApiAxiosParamCreator(configuration) - return { - /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. - * @summary Accept a membership invite - * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.acceptInvite(tenant, acceptInviteRequest, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['MembersApi.acceptInvite']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, /** - * Invites a member to a namespace. In enterprise and community instances, the member will automatically accept the invite and will have an `accepted` status. In cloud instances, the member will have a `pending` status until they accept the invite via an email sent to them. The invite is valid for **7 days**. If the member was previously invited and the invite is no longer valid, the same route will resend the invite. - * @summary Invite member - * @param {string} tenant Namespace\'s tenant ID - * @param {AddNamespaceMemberRequest} [addNamespaceMemberRequest] + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async addNamespaceMember(tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.addNamespaceMember(tenant, addNamespaceMemberRequest, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['MembersApi.addNamespaceMember']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + getMembershipInvitationList: async (filter?: string, page?: number, perPage?: number, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/users/invitations`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (filter !== undefined) { + localVarQueryParameter['filter'] = filter; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (perPage !== undefined) { + localVarQueryParameter['per_page'] = perPage; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; }, /** - * Generates a unique invitation link to invite a member to a namespace using their email. Each invitation link is unique and tied to the provided email. Upon accepting the invitation, the user\'s status will automatically be set to `accepted`. If the user associated with the email does not exist, the invitation link will redirect them to the signup page. The invitation remains valid for **7 days**. - * @summary Generate an invitation link for a namespace member + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace * @param {string} tenant Namespace\'s tenant ID - * @param {AddNamespaceMemberRequest} [addNamespaceMemberRequest] + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async generateInvitationLink(tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.generateInvitationLink(tenant, addNamespaceMemberRequest, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['MembersApi.generateInvitationLink']?.[localVarOperationServerIndex]?.url; + getNamespaceMembershipInvitationList: async (tenant: string, filter?: string, page?: number, perPage?: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('getNamespaceMembershipInvitationList', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (filter !== undefined) { + localVarQueryParameter['filter'] = filter; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (perPage !== undefined) { + localVarQueryParameter['per_page'] = perPage; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Allows the authenticated user to leave the specified namespace. Owners cannot leave a namespace; they must delete it instead. If the user attempts to leave their current authenticated namespace, the response will provide a new token that excludes this namespace. + * @summary Leave Namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + leaveNamespace: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('leaveNamespace', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/members` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication api-key required + await setApiKeyToObject(localVarHeaderParameter, "X-API-KEY", configuration) + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Clients may need to check a user\'s status before deciding whether to redirect to the accept-invite workflow or to the signup process. It is intended for use exclusively by clients in the `invite-member` pipeline. + * @summary Lookup User\'s Status + * @param {string} tenant The tenant ID of the namespace. + * @param {string} id The user\'s ID. + * @param {string} sig The signature included in the email. This is used instead of the user\'s token to authenticate the request. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + lookupUserStatus: async (tenant: string, id: string, sig: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('lookupUserStatus', 'tenant', tenant) + // verify required parameter 'id' is not null or undefined + assertParamExists('lookupUserStatus', 'id', id) + // verify required parameter 'sig' is not null or undefined + assertParamExists('lookupUserStatus', 'sig', sig) + const localVarPath = `/api/namespaces/{tenant}/members/{id}/accept-invite` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))) + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (sig !== undefined) { + localVarQueryParameter['sig'] = sig; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateMembershipInvitation: async (tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('updateMembershipInvitation', 'tenant', tenant) + // verify required parameter 'userId' is not null or undefined + assertParamExists('updateMembershipInvitation', 'userId', userId) + const localVarPath = `/api/namespaces/{tenant}/invitations/{user-id}` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))) + .replace(`{${"user-id"}}`, encodeURIComponent(String(userId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateNamespaceMemberRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * MembersApi - functional programming interface + */ +export const MembersApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = MembersApiAxiosParamCreator(configuration) + return { + /** + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. + * @summary Accept a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async acceptInvite(tenant: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.acceptInvite(tenant, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MembersApi.acceptInvite']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Invites a member to a namespace. In enterprise and community instances, the member will automatically accept the invite and will have an `accepted` status. In cloud instances, the member will have a `pending` status until they accept the invite via an email sent to them. The invite is valid for **7 days**. If the member was previously invited and the invite is no longer valid, the same route will resend the invite. + * @summary Invite member + * @param {string} tenant Namespace\'s tenant ID + * @param {AddNamespaceMemberRequest} [addNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async addNamespaceMember(tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.addNamespaceMember(tenant, addNamespaceMemberRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MembersApi.addNamespaceMember']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancelMembershipInvitation(tenant, userId, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MembersApi.cancelMembershipInvitation']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async declineInvite(tenant: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.declineInvite(tenant, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MembersApi.declineInvite']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Generates a unique invitation link to invite a member to a namespace using their email. Each invitation link is unique and tied to the provided email. Upon accepting the invitation, the user\'s status will automatically be set to `accepted`. If the user associated with the email does not exist, the invitation link will redirect them to the signup page. The invitation remains valid for **7 days**. + * @summary Generate an invitation link for a namespace member + * @param {string} tenant Namespace\'s tenant ID + * @param {AddNamespaceMemberRequest} [addNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async generateInvitationLink(tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.generateInvitationLink(tenant, addNamespaceMemberRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MembersApi.generateInvitationLink']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMembershipInvitationList(filter, page, perPage, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MembersApi.getMembershipInvitationList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MembersApi.getNamespaceMembershipInvitationList']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** @@ -22044,6 +22805,21 @@ export const MembersApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['MembersApi.lookupUserStatus']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MembersApi.updateMembershipInvitation']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, } }; @@ -22054,15 +22830,14 @@ export const MembersApiFactory = function (configuration?: Configuration, basePa const localVarFp = MembersApiFp(configuration) return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.acceptInvite(tenant, acceptInviteRequest, options).then((request) => request(axios, basePath)); + acceptInvite(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.acceptInvite(tenant, options).then((request) => request(axios, basePath)); }, /** * Invites a member to a namespace. In enterprise and community instances, the member will automatically accept the invite and will have an `accepted` status. In cloud instances, the member will have a `pending` status until they accept the invite via an email sent to them. The invite is valid for **7 days**. If the member was previously invited and the invite is no longer valid, the same route will resend the invite. @@ -22075,6 +22850,27 @@ export const MembersApiFactory = function (configuration?: Configuration, basePa addNamespaceMember(tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.addNamespaceMember(tenant, addNamespaceMemberRequest, options).then((request) => request(axios, basePath)); }, + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.cancelMembershipInvitation(tenant, userId, options).then((request) => request(axios, basePath)); + }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + declineInvite(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.declineInvite(tenant, options).then((request) => request(axios, basePath)); + }, /** * Generates a unique invitation link to invite a member to a namespace using their email. Each invitation link is unique and tied to the provided email. Upon accepting the invitation, the user\'s status will automatically be set to `accepted`. If the user associated with the email does not exist, the invitation link will redirect them to the signup page. The invitation remains valid for **7 days**. * @summary Generate an invitation link for a namespace member @@ -22086,6 +22882,31 @@ export const MembersApiFactory = function (configuration?: Configuration, basePa generateInvitationLink(tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.generateInvitationLink(tenant, addNamespaceMemberRequest, options).then((request) => request(axios, basePath)); }, + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.getMembershipInvitationList(filter, page, perPage, options).then((request) => request(axios, basePath)); + }, + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options).then((request) => request(axios, basePath)); + }, /** * Allows the authenticated user to leave the specified namespace. Owners cannot leave a namespace; they must delete it instead. If the user attempts to leave their current authenticated namespace, the response will provide a new token that excludes this namespace. * @summary Leave Namespace @@ -22108,6 +22929,18 @@ export const MembersApiFactory = function (configuration?: Configuration, basePa lookupUserStatus(tenant: string, id: string, sig: string, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.lookupUserStatus(tenant, id, sig, options).then((request) => request(axios, basePath)); }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options).then((request) => request(axios, basePath)); + }, }; }; @@ -22116,15 +22949,14 @@ export const MembersApiFactory = function (configuration?: Configuration, basePa */ export class MembersApi extends BaseAPI { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig) { - return MembersApiFp(this.configuration).acceptInvite(tenant, acceptInviteRequest, options).then((request) => request(this.axios, this.basePath)); + public acceptInvite(tenant: string, options?: RawAxiosRequestConfig) { + return MembersApiFp(this.configuration).acceptInvite(tenant, options).then((request) => request(this.axios, this.basePath)); } /** @@ -22139,6 +22971,29 @@ export class MembersApi extends BaseAPI { return MembersApiFp(this.configuration).addNamespaceMember(tenant, addNamespaceMemberRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig) { + return MembersApiFp(this.configuration).cancelMembershipInvitation(tenant, userId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public declineInvite(tenant: string, options?: RawAxiosRequestConfig) { + return MembersApiFp(this.configuration).declineInvite(tenant, options).then((request) => request(this.axios, this.basePath)); + } + /** * Generates a unique invitation link to invite a member to a namespace using their email. Each invitation link is unique and tied to the provided email. Upon accepting the invitation, the user\'s status will automatically be set to `accepted`. If the user associated with the email does not exist, the invitation link will redirect them to the signup page. The invitation remains valid for **7 days**. * @summary Generate an invitation link for a namespace member @@ -22151,6 +23006,33 @@ export class MembersApi extends BaseAPI { return MembersApiFp(this.configuration).generateInvitationLink(tenant, addNamespaceMemberRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig) { + return MembersApiFp(this.configuration).getMembershipInvitationList(filter, page, perPage, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig) { + return MembersApiFp(this.configuration).getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options).then((request) => request(this.axios, this.basePath)); + } + /** * Allows the authenticated user to leave the specified namespace. Owners cannot leave a namespace; they must delete it instead. If the user attempts to leave their current authenticated namespace, the response will provide a new token that excludes this namespace. * @summary Leave Namespace @@ -22174,6 +23056,19 @@ export class MembersApi extends BaseAPI { public lookupUserStatus(tenant: string, id: string, sig: string, options?: RawAxiosRequestConfig) { return MembersApiFp(this.configuration).lookupUserStatus(tenant, id, sig, options).then((request) => request(this.axios, this.basePath)); } + + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig) { + return MembersApiFp(this.configuration).updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options).then((request) => request(this.axios, this.basePath)); + } } @@ -22705,17 +23600,16 @@ export class MfaApi extends BaseAPI { export const NamespacesApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - acceptInvite: async (tenant: string, acceptInviteRequest?: AcceptInviteRequest, options: RawAxiosRequestConfig = {}): Promise => { + acceptInvite: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined assertParamExists('acceptInvite', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}/members/accept-invite` + const localVarPath = `/api/namespaces/{tenant}/invitations/accept` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -22734,12 +23628,9 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(acceptInviteRequest, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -22963,6 +23854,48 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur options: localVarRequestOptions, }; }, + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelMembershipInvitation: async (tenant: string, userId: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('cancelMembershipInvitation', 'tenant', tenant) + // verify required parameter 'userId' is not null or undefined + assertParamExists('cancelMembershipInvitation', 'userId', userId) + const localVarPath = `/api/namespaces/{tenant}/invitations/{user-id}` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))) + .replace(`{${"user-id"}}`, encodeURIComponent(String(userId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Create a new connector. * @summary Connector\'s create @@ -23311,6 +24244,44 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur options: localVarRequestOptions, }; }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + declineInvite: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('declineInvite', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations/decline` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Delete a namespace. * @summary Delete namespace @@ -23537,14 +24508,104 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur * Generates a unique invitation link to invite a member to a namespace using their email. Each invitation link is unique and tied to the provided email. Upon accepting the invitation, the user\'s status will automatically be set to `accepted`. If the user associated with the email does not exist, the invitation link will redirect them to the signup page. The invitation remains valid for **7 days**. * @summary Generate an invitation link for a namespace member * @param {string} tenant Namespace\'s tenant ID - * @param {AddNamespaceMemberRequest} [addNamespaceMemberRequest] + * @param {AddNamespaceMemberRequest} [addNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + generateInvitationLink: async (tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('generateInvitationLink', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations/links` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(addNamespaceMemberRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMembershipInvitationList: async (filter?: string, page?: number, perPage?: number, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/users/invitations`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (filter !== undefined) { + localVarQueryParameter['filter'] = filter; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (perPage !== undefined) { + localVarQueryParameter['per_page'] = perPage; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Get a namespace. + * @summary Get a namespace + * @param {string} tenant Namespace\'s tenant ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - generateInvitationLink: async (tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options: RawAxiosRequestConfig = {}): Promise => { + getNamespace: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined - assertParamExists('generateInvitationLink', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}/members/invites` + assertParamExists('getNamespace', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -23553,22 +24614,22 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication api-key required + await setApiKeyToObject(localVarHeaderParameter, "X-API-KEY", configuration) + // authentication jwt required // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(addNamespaceMemberRequest, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -23577,15 +24638,15 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur }, /** * Get a namespace. - * @summary Get a namespace + * @summary Get namespace admin * @param {string} tenant Namespace\'s tenant ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getNamespace: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { + getNamespaceAdmin: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined - assertParamExists('getNamespace', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}` + assertParamExists('getNamespaceAdmin', 'tenant', tenant) + const localVarPath = `/admin/api/namespaces/{tenant}` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -23617,16 +24678,19 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur }; }, /** - * Get a namespace. - * @summary Get namespace admin + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getNamespaceAdmin: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { + getNamespaceMembershipInvitationList: async (tenant: string, filter?: string, page?: number, perPage?: number, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined - assertParamExists('getNamespaceAdmin', 'tenant', tenant) - const localVarPath = `/admin/api/namespaces/{tenant}` + assertParamExists('getNamespaceMembershipInvitationList', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -23639,13 +24703,22 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - // authentication api-key required - await setApiKeyToObject(localVarHeaderParameter, "X-API-KEY", configuration) - // authentication jwt required // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (filter !== undefined) { + localVarQueryParameter['filter'] = filter; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (perPage !== undefined) { + localVarQueryParameter['per_page'] = perPage; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -24016,6 +25089,52 @@ export const NamespacesApiAxiosParamCreator = function (configuration?: Configur options: localVarRequestOptions, }; }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateMembershipInvitation: async (tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('updateMembershipInvitation', 'tenant', tenant) + // verify required parameter 'userId' is not null or undefined + assertParamExists('updateMembershipInvitation', 'userId', userId) + const localVarPath = `/api/namespaces/{tenant}/invitations/{user-id}` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))) + .replace(`{${"user-id"}}`, encodeURIComponent(String(userId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateNamespaceMemberRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Update a member role from a namespace. * @summary Update a member from a namespace @@ -24072,15 +25191,14 @@ export const NamespacesApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = NamespacesApiAxiosParamCreator(configuration) return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.acceptInvite(tenant, acceptInviteRequest, options); + async acceptInvite(tenant: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.acceptInvite(tenant, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['NamespacesApi.acceptInvite']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -24155,6 +25273,20 @@ export const NamespacesApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['NamespacesApi.apiKeyUpdate']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancelMembershipInvitation(tenant, userId, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['NamespacesApi.cancelMembershipInvitation']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Create a new connector. * @summary Connector\'s create @@ -24263,6 +25395,19 @@ export const NamespacesApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['NamespacesApi.createNamespaceAdmin']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async declineInvite(tenant: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.declineInvite(tenant, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['NamespacesApi.declineInvite']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Delete a namespace. * @summary Delete namespace @@ -24347,6 +25492,21 @@ export const NamespacesApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['NamespacesApi.generateInvitationLink']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMembershipInvitationList(filter, page, perPage, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['NamespacesApi.getMembershipInvitationList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Get a namespace. * @summary Get a namespace @@ -24373,6 +25533,22 @@ export const NamespacesApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['NamespacesApi.getNamespaceAdmin']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['NamespacesApi.getNamespaceMembershipInvitationList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Get a namespace support identifier. * @summary Get a namespace support identifier. @@ -24486,6 +25662,21 @@ export const NamespacesApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['NamespacesApi.removeNamespaceMember']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['NamespacesApi.updateMembershipInvitation']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Update a member role from a namespace. * @summary Update a member from a namespace @@ -24511,15 +25702,14 @@ export const NamespacesApiFactory = function (configuration?: Configuration, bas const localVarFp = NamespacesApiFp(configuration) return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.acceptInvite(tenant, acceptInviteRequest, options).then((request) => request(axios, basePath)); + acceptInvite(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.acceptInvite(tenant, options).then((request) => request(axios, basePath)); }, /** * Invites a member to a namespace. In enterprise and community instances, the member will automatically accept the invite and will have an `accepted` status. In cloud instances, the member will have a `pending` status until they accept the invite via an email sent to them. The invite is valid for **7 days**. If the member was previously invited and the invite is no longer valid, the same route will resend the invite. @@ -24576,6 +25766,17 @@ export const NamespacesApiFactory = function (configuration?: Configuration, bas apiKeyUpdate(key: string, apiKeyUpdate?: ApiKeyUpdate, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.apiKeyUpdate(key, apiKeyUpdate, options).then((request) => request(axios, basePath)); }, + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.cancelMembershipInvitation(tenant, userId, options).then((request) => request(axios, basePath)); + }, /** * Create a new connector. * @summary Connector\'s create @@ -24660,6 +25861,16 @@ export const NamespacesApiFactory = function (configuration?: Configuration, bas createNamespaceAdmin(tenant: string, createNamespaceRequest?: CreateNamespaceRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.createNamespaceAdmin(tenant, createNamespaceRequest, options).then((request) => request(axios, basePath)); }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + declineInvite(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.declineInvite(tenant, options).then((request) => request(axios, basePath)); + }, /** * Delete a namespace. * @summary Delete namespace @@ -24726,6 +25937,18 @@ export const NamespacesApiFactory = function (configuration?: Configuration, bas generateInvitationLink(tenant: string, addNamespaceMemberRequest?: AddNamespaceMemberRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.generateInvitationLink(tenant, addNamespaceMemberRequest, options).then((request) => request(axios, basePath)); }, + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.getMembershipInvitationList(filter, page, perPage, options).then((request) => request(axios, basePath)); + }, /** * Get a namespace. * @summary Get a namespace @@ -24746,6 +25969,19 @@ export const NamespacesApiFactory = function (configuration?: Configuration, bas getNamespaceAdmin(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.getNamespaceAdmin(tenant, options).then((request) => request(axios, basePath)); }, + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options).then((request) => request(axios, basePath)); + }, /** * Get a namespace support identifier. * @summary Get a namespace support identifier. @@ -24835,6 +26071,18 @@ export const NamespacesApiFactory = function (configuration?: Configuration, bas removeNamespaceMember(tenant: string, uid: string, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.removeNamespaceMember(tenant, uid, options).then((request) => request(axios, basePath)); }, + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options).then((request) => request(axios, basePath)); + }, /** * Update a member role from a namespace. * @summary Update a member from a namespace @@ -24855,15 +26103,14 @@ export const NamespacesApiFactory = function (configuration?: Configuration, bas */ export class NamespacesApi extends BaseAPI { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig) { - return NamespacesApiFp(this.configuration).acceptInvite(tenant, acceptInviteRequest, options).then((request) => request(this.axios, this.basePath)); + public acceptInvite(tenant: string, options?: RawAxiosRequestConfig) { + return NamespacesApiFp(this.configuration).acceptInvite(tenant, options).then((request) => request(this.axios, this.basePath)); } /** @@ -24926,6 +26173,18 @@ export class NamespacesApi extends BaseAPI { return NamespacesApiFp(this.configuration).apiKeyUpdate(key, apiKeyUpdate, options).then((request) => request(this.axios, this.basePath)); } + /** + * Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + * @summary Cancel a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public cancelMembershipInvitation(tenant: string, userId: string, options?: RawAxiosRequestConfig) { + return NamespacesApiFp(this.configuration).cancelMembershipInvitation(tenant, userId, options).then((request) => request(this.axios, this.basePath)); + } + /** * Create a new connector. * @summary Connector\'s create @@ -25018,6 +26277,17 @@ export class NamespacesApi extends BaseAPI { return NamespacesApiFp(this.configuration).createNamespaceAdmin(tenant, createNamespaceRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public declineInvite(tenant: string, options?: RawAxiosRequestConfig) { + return NamespacesApiFp(this.configuration).declineInvite(tenant, options).then((request) => request(this.axios, this.basePath)); + } + /** * Delete a namespace. * @summary Delete namespace @@ -25090,6 +26360,19 @@ export class NamespacesApi extends BaseAPI { return NamespacesApiFp(this.configuration).generateInvitationLink(tenant, addNamespaceMemberRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + * @summary Get membership invitations for the authenticated user + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public getMembershipInvitationList(filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig) { + return NamespacesApiFp(this.configuration).getMembershipInvitationList(filter, page, perPage, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get a namespace. * @summary Get a namespace @@ -25112,6 +26395,20 @@ export class NamespacesApi extends BaseAPI { return NamespacesApiFp(this.configuration).getNamespaceAdmin(tenant, options).then((request) => request(this.axios, this.basePath)); } + /** + * Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + * @summary Get membership invitations for a namespace + * @param {string} tenant Namespace\'s tenant ID + * @param {string} [filter] Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. + * @param {number} [page] Page number + * @param {number} [perPage] Items per page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public getNamespaceMembershipInvitationList(tenant: string, filter?: string, page?: number, perPage?: number, options?: RawAxiosRequestConfig) { + return NamespacesApiFp(this.configuration).getNamespaceMembershipInvitationList(tenant, filter, page, perPage, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get a namespace support identifier. * @summary Get a namespace support identifier. @@ -25209,6 +26506,19 @@ export class NamespacesApi extends BaseAPI { return NamespacesApiFp(this.configuration).removeNamespaceMember(tenant, uid, options).then((request) => request(this.axios, this.basePath)); } + /** + * Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + * @summary Update a pending membership invitation + * @param {string} tenant Namespace\'s tenant ID + * @param {string} userId The ID of the invited user + * @param {UpdateNamespaceMemberRequest} [updateNamespaceMemberRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public updateMembershipInvitation(tenant: string, userId: string, updateNamespaceMemberRequest?: UpdateNamespaceMemberRequest, options?: RawAxiosRequestConfig) { + return NamespacesApiFp(this.configuration).updateMembershipInvitation(tenant, userId, updateNamespaceMemberRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * Update a member role from a namespace. * @summary Update a member from a namespace @@ -28961,17 +30271,16 @@ export class TunnelsApi extends BaseAPI { export const UsersApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - acceptInvite: async (tenant: string, acceptInviteRequest?: AcceptInviteRequest, options: RawAxiosRequestConfig = {}): Promise => { + acceptInvite: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'tenant' is not null or undefined assertParamExists('acceptInvite', 'tenant', tenant) - const localVarPath = `/api/namespaces/{tenant}/members/accept-invite` + const localVarPath = `/api/namespaces/{tenant}/invitations/accept` .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -28990,12 +30299,9 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(acceptInviteRequest, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -29272,6 +30578,44 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + declineInvite: async (tenant: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tenant' is not null or undefined + assertParamExists('declineInvite', 'tenant', tenant) + const localVarPath = `/api/namespaces/{tenant}/invitations/decline` + .replace(`{${"tenant"}}`, encodeURIComponent(String(tenant))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication jwt required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Deletes the authenticated user. The user will be removed from any namespaces they are a member of. Users who are owners of namespaces cannot be deleted. In such cases, the user must delete the namespace(s) first. > NOTE: This route is available only for **cloud** instances. Enterprise users must use the admin console, and community users must use the CLI. * @summary Delete user @@ -30170,15 +31514,14 @@ export const UsersApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration) return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.acceptInvite(tenant, acceptInviteRequest, options); + async acceptInvite(tenant: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.acceptInvite(tenant, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['UsersApi.acceptInvite']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -30274,6 +31617,19 @@ export const UsersApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['UsersApi.createUserAdmin']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async declineInvite(tenant: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.declineInvite(tenant, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['UsersApi.declineInvite']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Deletes the authenticated user. The user will be removed from any namespaces they are a member of. Users who are owners of namespaces cannot be deleted. In such cases, the user must delete the namespace(s) first. > NOTE: This route is available only for **cloud** instances. Enterprise users must use the admin console, and community users must use the CLI. * @summary Delete user @@ -30592,15 +31948,14 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath const localVarFp = UsersApiFp(configuration) return { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.acceptInvite(tenant, acceptInviteRequest, options).then((request) => request(axios, basePath)); + acceptInvite(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.acceptInvite(tenant, options).then((request) => request(axios, basePath)); }, /** * Delete a user. @@ -30672,6 +32027,16 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath createUserAdmin(userAdminRequest?: UserAdminRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.createUserAdmin(userAdminRequest, options).then((request) => request(axios, basePath)); }, + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + declineInvite(tenant: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.declineInvite(tenant, options).then((request) => request(axios, basePath)); + }, /** * Deletes the authenticated user. The user will be removed from any namespaces they are a member of. Users who are owners of namespaces cannot be deleted. In such cases, the user must delete the namespace(s) first. > NOTE: This route is available only for **cloud** instances. Enterprise users must use the admin console, and community users must use the CLI. * @summary Delete user @@ -30919,15 +32284,14 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath */ export class UsersApi extends BaseAPI { /** - * This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. + * Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. * @summary Accept a membership invite * @param {string} tenant Namespace\'s tenant ID - * @param {AcceptInviteRequest} [acceptInviteRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public acceptInvite(tenant: string, acceptInviteRequest?: AcceptInviteRequest, options?: RawAxiosRequestConfig) { - return UsersApiFp(this.configuration).acceptInvite(tenant, acceptInviteRequest, options).then((request) => request(this.axios, this.basePath)); + public acceptInvite(tenant: string, options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).acceptInvite(tenant, options).then((request) => request(this.axios, this.basePath)); } /** @@ -31007,6 +32371,17 @@ export class UsersApi extends BaseAPI { return UsersApiFp(this.configuration).createUserAdmin(userAdminRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + * @summary Decline a membership invite + * @param {string} tenant Namespace\'s tenant ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public declineInvite(tenant: string, options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).declineInvite(tenant, options).then((request) => request(this.axios, this.basePath)); + } + /** * Deletes the authenticated user. The user will be removed from any namespaces they are a member of. Users who are owners of namespaces cannot be deleted. In such cases, the user must delete the namespace(s) first. > NOTE: This route is available only for **cloud** instances. Enterprise users must use the admin console, and community users must use the CLI. * @summary Delete user diff --git a/ui/src/api/client/docs/CloudApi.md b/ui/src/api/client/docs/CloudApi.md index 2ded4b9bbf8..7aff47fd0e7 100644 --- a/ui/src/api/client/docs/CloudApi.md +++ b/ui/src/api/client/docs/CloudApi.md @@ -4,9 +4,10 @@ All URIs are relative to *http://localhost* |Method | HTTP request | Description| |------------- | ------------- | -------------| -|[**acceptInvite**](#acceptinvite) | **PATCH** /api/namespaces/{tenant}/members/accept-invite | Accept a membership invite| +|[**acceptInvite**](#acceptinvite) | **PATCH** /api/namespaces/{tenant}/invitations/accept | Accept a membership invite| |[**attachPaymentMethod**](#attachpaymentmethod) | **POST** /api/billing/paymentmethod/attach | Attach payment method| |[**authMFA**](#authmfa) | **POST** /api/user/mfa/auth | Auth MFA| +|[**cancelMembershipInvitation**](#cancelmembershipinvitation) | **DELETE** /api/namespaces/{tenant}/invitations/{user-id} | Cancel a pending membership invitation| |[**choiceDevices**](#choicedevices) | **POST** /api/billing/device-choice | Choice devices| |[**clsoeSession**](#clsoesession) | **POST** /api/sessions/{uid}/close | Close session| |[**connectorCreate**](#connectorcreate) | **POST** /api/connector | Connector\'s create| @@ -21,6 +22,7 @@ All URIs are relative to *http://localhost* |[**createSubscription**](#createsubscription) | **POST** /api/billing/subscription | Create subscription| |[**createTunnel**](#createtunnel) | **POST** /api/devices/{uid}/tunnels | Create a tunnel| |[**createWebEndpoint**](#createwebendpoint) | **POST** /api/web-endpoints | Create a web-endpoint| +|[**declineInvite**](#declineinvite) | **PATCH** /api/namespaces/{tenant}/invitations/decline | Decline a membership invite| |[**deleteAnnouncement**](#deleteannouncement) | **DELETE** /admin/api/announcements/{uuid} | Delete an announcement| |[**deleteFirewallRule**](#deletefirewallrule) | **DELETE** /api/firewall/rules/{id} | Delete firewall rule| |[**deleteSessionRecord**](#deletesessionrecord) | **DELETE** /api/sessions/{uid}/records/{seat} | Delete session record| @@ -31,13 +33,15 @@ All URIs are relative to *http://localhost* |[**disableMFA**](#disablemfa) | **PUT** /api/user/mfa/disable | Disable MFA| |[**enableMFA**](#enablemfa) | **PUT** /api/user/mfa/enable | Enable MFA| |[**evaluate**](#evaluate) | **POST** /api/billing/evaluate | Evaluate| -|[**generateInvitationLink**](#generateinvitationlink) | **POST** /api/namespaces/{tenant}/members/invites | Generate an invitation link for a namespace member| +|[**generateInvitationLink**](#generateinvitationlink) | **POST** /api/namespaces/{tenant}/invitations/links | Generate an invitation link for a namespace member| |[**generateMFA**](#generatemfa) | **GET** /api/user/mfa/generate | Generate MFA Credentials| |[**getAnnouncementAdmin**](#getannouncementadmin) | **GET** /admin/api/announcements/{uuid} | Get a announcement| |[**getCustomer**](#getcustomer) | **GET** /api/billing/customer | Get Customer| |[**getDevicesMostUsed**](#getdevicesmostused) | **GET** /api/billing/devices-most-used | Get devices most used| |[**getFirewallRule**](#getfirewallrule) | **GET** /api/firewall/rules/{id} | Get firewall rule| |[**getFirewallRules**](#getfirewallrules) | **GET** /api/firewall/rules | Get firewall rules| +|[**getMembershipInvitationList**](#getmembershipinvitationlist) | **GET** /api/users/invitations | Get membership invitations for the authenticated user| +|[**getNamespaceMembershipInvitationList**](#getnamespacemembershipinvitationlist) | **GET** /api/namespaces/{tenant}/invitations | Get membership invitations for a namespace| |[**getNamespaceSupport**](#getnamespacesupport) | **GET** /api/namespaces/{tenant}/support | Get a namespace support identifier.| |[**getSamlAuthUrl**](#getsamlauthurl) | **GET** /api/user/saml/auth | Get SAML authentication URL| |[**getSessionRecord**](#getsessionrecord) | **GET** /api/sessions/{uid}/records/{seat} | Get session record| @@ -57,31 +61,29 @@ All URIs are relative to *http://localhost* |[**setDefaultPaymentMethod**](#setdefaultpaymentmethod) | **POST** /api/billing/paymentmethod/default | Set default payment method| |[**updateAnnouncement**](#updateannouncement) | **PUT** /admin/api/announcements/{uuid} | Update an announcement| |[**updateFirewallRule**](#updatefirewallrule) | **PUT** /api/firewall/rules/{id} | Update firewall rule| +|[**updateMembershipInvitation**](#updatemembershipinvitation) | **PATCH** /api/namespaces/{tenant}/invitations/{user-id} | Update a pending membership invitation| |[**updateRecoverPassword**](#updaterecoverpassword) | **POST** /api/user/{uid}/update_password | Update user password| # **acceptInvite** > acceptInvite() -This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. +Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. ### Example ```typescript import { CloudApi, - Configuration, - AcceptInviteRequest + Configuration } from './api'; const configuration = new Configuration(); const apiInstance = new CloudApi(configuration); let tenant: string; //Namespace\'s tenant ID (default to undefined) -let acceptInviteRequest: AcceptInviteRequest; // (optional) const { status, data } = await apiInstance.acceptInvite( - tenant, - acceptInviteRequest + tenant ); ``` @@ -89,7 +91,6 @@ const { status, data } = await apiInstance.acceptInvite( |Name | Type | Description | Notes| |------------- | ------------- | ------------- | -------------| -| **acceptInviteRequest** | **AcceptInviteRequest**| | | | **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| @@ -103,7 +104,7 @@ void (empty response body) ### HTTP request headers - - **Content-Type**: application/json + - **Content-Type**: Not defined - **Accept**: application/json @@ -234,6 +235,65 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **cancelMembershipInvitation** +> cancelMembershipInvitation() + +Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + +### Example + +```typescript +import { + CloudApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new CloudApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let userId: string; //The ID of the invited user (default to undefined) + +const { status, data } = await apiInstance.cancelMembershipInvitation( + tenant, + userId +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **userId** | [**string**] | The ID of the invited user | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully cancelled | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **choiceDevices** > choiceDevices() @@ -1017,6 +1077,62 @@ const { status, data } = await apiInstance.createWebEndpoint( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **declineInvite** +> declineInvite() + +Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + +### Example + +```typescript +import { + CloudApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new CloudApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) + +const { status, data } = await apiInstance.declineInvite( + tenant +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully declined | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **deleteAnnouncement** > Announcement deleteAnnouncement() @@ -1931,6 +2047,129 @@ const { status, data } = await apiInstance.getFirewallRules( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getMembershipInvitationList** +> Array getMembershipInvitationList() + +Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + +### Example + +```typescript +import { + CloudApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new CloudApi(configuration); + +let filter: string; //Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. (optional) (default to undefined) +let page: number; //Page number (optional) (default to 1) +let perPage: number; //Items per page (optional) (default to 10) + +const { status, data } = await apiInstance.getMembershipInvitationList( + filter, + page, + perPage +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **filter** | [**string**] | Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. | (optional) defaults to undefined| +| **page** | [**number**] | Page number | (optional) defaults to 1| +| **perPage** | [**number**] | Items per page | (optional) defaults to 10| + + +### Return type + +**Array** + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successfully retrieved membership invitations list. | * X-Total-Count - Total number of membership invitations.
| +|**401** | Unauthorized | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getNamespaceMembershipInvitationList** +> Array getNamespaceMembershipInvitationList() + +Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + +### Example + +```typescript +import { + CloudApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new CloudApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let filter: string; //Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. (optional) (default to undefined) +let page: number; //Page number (optional) (default to 1) +let perPage: number; //Items per page (optional) (default to 10) + +const { status, data } = await apiInstance.getNamespaceMembershipInvitationList( + tenant, + filter, + page, + perPage +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **filter** | [**string**] | Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. | (optional) defaults to undefined| +| **page** | [**number**] | Page number | (optional) defaults to 1| +| **perPage** | [**number**] | Items per page | (optional) defaults to 10| + + +### Return type + +**Array** + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successfully retrieved namespace membership invitations list. | * X-Total-Count - Total number of membership invitations.
| +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **getNamespaceSupport** > Support getNamespaceSupport() @@ -3017,6 +3256,69 @@ const { status, data } = await apiInstance.updateFirewallRule( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **updateMembershipInvitation** +> updateMembershipInvitation() + +Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + +### Example + +```typescript +import { + CloudApi, + Configuration, + UpdateNamespaceMemberRequest +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new CloudApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let userId: string; //The ID of the invited user (default to undefined) +let updateNamespaceMemberRequest: UpdateNamespaceMemberRequest; // (optional) + +const { status, data } = await apiInstance.updateMembershipInvitation( + tenant, + userId, + updateNamespaceMemberRequest +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **updateNamespaceMemberRequest** | **UpdateNamespaceMemberRequest**| | | +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **userId** | [**string**] | The ID of the invited user | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully updated | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **updateRecoverPassword** > updateRecoverPassword() diff --git a/ui/src/api/client/docs/MembersApi.md b/ui/src/api/client/docs/MembersApi.md index 44f171b9f5c..dec5e6d5ff3 100644 --- a/ui/src/api/client/docs/MembersApi.md +++ b/ui/src/api/client/docs/MembersApi.md @@ -4,35 +4,37 @@ All URIs are relative to *http://localhost* |Method | HTTP request | Description| |------------- | ------------- | -------------| -|[**acceptInvite**](#acceptinvite) | **PATCH** /api/namespaces/{tenant}/members/accept-invite | Accept a membership invite| +|[**acceptInvite**](#acceptinvite) | **PATCH** /api/namespaces/{tenant}/invitations/accept | Accept a membership invite| |[**addNamespaceMember**](#addnamespacemember) | **POST** /api/namespaces/{tenant}/members | Invite member| -|[**generateInvitationLink**](#generateinvitationlink) | **POST** /api/namespaces/{tenant}/members/invites | Generate an invitation link for a namespace member| +|[**cancelMembershipInvitation**](#cancelmembershipinvitation) | **DELETE** /api/namespaces/{tenant}/invitations/{user-id} | Cancel a pending membership invitation| +|[**declineInvite**](#declineinvite) | **PATCH** /api/namespaces/{tenant}/invitations/decline | Decline a membership invite| +|[**generateInvitationLink**](#generateinvitationlink) | **POST** /api/namespaces/{tenant}/invitations/links | Generate an invitation link for a namespace member| +|[**getMembershipInvitationList**](#getmembershipinvitationlist) | **GET** /api/users/invitations | Get membership invitations for the authenticated user| +|[**getNamespaceMembershipInvitationList**](#getnamespacemembershipinvitationlist) | **GET** /api/namespaces/{tenant}/invitations | Get membership invitations for a namespace| |[**leaveNamespace**](#leavenamespace) | **DELETE** /api/namespaces/{tenant}/members | Leave Namespace| |[**lookupUserStatus**](#lookupuserstatus) | **GET** /api/namespaces/{tenant}/members/{id}/accept-invite | Lookup User\'s Status| +|[**updateMembershipInvitation**](#updatemembershipinvitation) | **PATCH** /api/namespaces/{tenant}/invitations/{user-id} | Update a pending membership invitation| # **acceptInvite** > acceptInvite() -This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. +Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. ### Example ```typescript import { MembersApi, - Configuration, - AcceptInviteRequest + Configuration } from './api'; const configuration = new Configuration(); const apiInstance = new MembersApi(configuration); let tenant: string; //Namespace\'s tenant ID (default to undefined) -let acceptInviteRequest: AcceptInviteRequest; // (optional) const { status, data } = await apiInstance.acceptInvite( - tenant, - acceptInviteRequest + tenant ); ``` @@ -40,7 +42,6 @@ const { status, data } = await apiInstance.acceptInvite( |Name | Type | Description | Notes| |------------- | ------------- | ------------- | -------------| -| **acceptInviteRequest** | **AcceptInviteRequest**| | | | **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| @@ -54,7 +55,7 @@ void (empty response body) ### HTTP request headers - - **Content-Type**: application/json + - **Content-Type**: Not defined - **Accept**: application/json @@ -131,6 +132,121 @@ const { status, data } = await apiInstance.addNamespaceMember( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **cancelMembershipInvitation** +> cancelMembershipInvitation() + +Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + +### Example + +```typescript +import { + MembersApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new MembersApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let userId: string; //The ID of the invited user (default to undefined) + +const { status, data } = await apiInstance.cancelMembershipInvitation( + tenant, + userId +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **userId** | [**string**] | The ID of the invited user | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully cancelled | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **declineInvite** +> declineInvite() + +Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + +### Example + +```typescript +import { + MembersApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new MembersApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) + +const { status, data } = await apiInstance.declineInvite( + tenant +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully declined | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **generateInvitationLink** > GenerateInvitationLink200Response generateInvitationLink() @@ -192,6 +308,129 @@ const { status, data } = await apiInstance.generateInvitationLink( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getMembershipInvitationList** +> Array getMembershipInvitationList() + +Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + +### Example + +```typescript +import { + MembersApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new MembersApi(configuration); + +let filter: string; //Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. (optional) (default to undefined) +let page: number; //Page number (optional) (default to 1) +let perPage: number; //Items per page (optional) (default to 10) + +const { status, data } = await apiInstance.getMembershipInvitationList( + filter, + page, + perPage +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **filter** | [**string**] | Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. | (optional) defaults to undefined| +| **page** | [**number**] | Page number | (optional) defaults to 1| +| **perPage** | [**number**] | Items per page | (optional) defaults to 10| + + +### Return type + +**Array** + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successfully retrieved membership invitations list. | * X-Total-Count - Total number of membership invitations.
| +|**401** | Unauthorized | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getNamespaceMembershipInvitationList** +> Array getNamespaceMembershipInvitationList() + +Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + +### Example + +```typescript +import { + MembersApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new MembersApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let filter: string; //Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. (optional) (default to undefined) +let page: number; //Page number (optional) (default to 1) +let perPage: number; //Items per page (optional) (default to 10) + +const { status, data } = await apiInstance.getNamespaceMembershipInvitationList( + tenant, + filter, + page, + perPage +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **filter** | [**string**] | Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. | (optional) defaults to undefined| +| **page** | [**number**] | Page number | (optional) defaults to 1| +| **perPage** | [**number**] | Items per page | (optional) defaults to 10| + + +### Return type + +**Array** + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successfully retrieved namespace membership invitations list. | * X-Total-Count - Total number of membership invitations.
| +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **leaveNamespace** > UserAuth leaveNamespace() @@ -307,3 +546,66 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **updateMembershipInvitation** +> updateMembershipInvitation() + +Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + +### Example + +```typescript +import { + MembersApi, + Configuration, + UpdateNamespaceMemberRequest +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new MembersApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let userId: string; //The ID of the invited user (default to undefined) +let updateNamespaceMemberRequest: UpdateNamespaceMemberRequest; // (optional) + +const { status, data } = await apiInstance.updateMembershipInvitation( + tenant, + userId, + updateNamespaceMemberRequest +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **updateNamespaceMemberRequest** | **UpdateNamespaceMemberRequest**| | | +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **userId** | [**string**] | The ID of the invited user | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully updated | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/ui/src/api/client/docs/MembershipInvitation.md b/ui/src/api/client/docs/MembershipInvitation.md new file mode 100644 index 00000000000..30d2ffe84e8 --- /dev/null +++ b/ui/src/api/client/docs/MembershipInvitation.md @@ -0,0 +1,37 @@ +# MembershipInvitation + +A membership invitation to a namespace + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**namespace** | [**MembershipInvitationNamespace**](MembershipInvitationNamespace.md) | | [optional] [default to undefined] +**user** | [**MembershipInvitationUser**](MembershipInvitationUser.md) | | [optional] [default to undefined] +**invited_by** | **string** | The ID of the user who sent the invitation | [optional] [default to undefined] +**created_at** | **string** | When the invitation was created | [optional] [default to undefined] +**updated_at** | **string** | When the invitation was last updated | [optional] [default to undefined] +**expires_at** | **string** | When the invitation expires | [optional] [default to undefined] +**status** | [**MembershipInvitationStatus**](MembershipInvitationStatus.md) | | [optional] [default to undefined] +**status_updated_at** | **string** | When the status was last updated | [optional] [default to undefined] +**role** | [**NamespaceMemberRole**](NamespaceMemberRole.md) | | [optional] [default to undefined] + +## Example + +```typescript +import { MembershipInvitation } from './api'; + +const instance: MembershipInvitation = { + namespace, + user, + invited_by, + created_at, + updated_at, + expires_at, + status, + status_updated_at, + role, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/ui/src/api/client/docs/MembershipInvitationNamespace.md b/ui/src/api/client/docs/MembershipInvitationNamespace.md new file mode 100644 index 00000000000..c67b899a2ef --- /dev/null +++ b/ui/src/api/client/docs/MembershipInvitationNamespace.md @@ -0,0 +1,23 @@ +# MembershipInvitationNamespace + +The namespace associated with this invitation + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**tenant_id** | **string** | The namespace tenant ID | [default to undefined] +**name** | **string** | The namespace name | [default to undefined] + +## Example + +```typescript +import { MembershipInvitationNamespace } from './api'; + +const instance: MembershipInvitationNamespace = { + tenant_id, + name, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/ui/src/api/client/docs/MembershipInvitationStatus.md b/ui/src/api/client/docs/MembershipInvitationStatus.md new file mode 100644 index 00000000000..ae5a76593e9 --- /dev/null +++ b/ui/src/api/client/docs/MembershipInvitationStatus.md @@ -0,0 +1,15 @@ +# MembershipInvitationStatus + +The current status of the invitation + +## Enum + +* `Pending` (value: `'pending'`) + +* `Accepted` (value: `'accepted'`) + +* `Rejected` (value: `'rejected'`) + +* `Cancelled` (value: `'cancelled'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/ui/src/api/client/docs/MembershipInvitationUser.md b/ui/src/api/client/docs/MembershipInvitationUser.md new file mode 100644 index 00000000000..dee726c09bb --- /dev/null +++ b/ui/src/api/client/docs/MembershipInvitationUser.md @@ -0,0 +1,23 @@ +# MembershipInvitationUser + +The invited user + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **string** | The ID of the invited user | [default to undefined] +**email** | **string** | The email of the invited user | [default to undefined] + +## Example + +```typescript +import { MembershipInvitationUser } from './api'; + +const instance: MembershipInvitationUser = { + id, + email, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/ui/src/api/client/docs/NamespacesApi.md b/ui/src/api/client/docs/NamespacesApi.md index 4b14c6d70ea..30a5ad619e9 100644 --- a/ui/src/api/client/docs/NamespacesApi.md +++ b/ui/src/api/client/docs/NamespacesApi.md @@ -4,12 +4,13 @@ All URIs are relative to *http://localhost* |Method | HTTP request | Description| |------------- | ------------- | -------------| -|[**acceptInvite**](#acceptinvite) | **PATCH** /api/namespaces/{tenant}/members/accept-invite | Accept a membership invite| +|[**acceptInvite**](#acceptinvite) | **PATCH** /api/namespaces/{tenant}/invitations/accept | Accept a membership invite| |[**addNamespaceMember**](#addnamespacemember) | **POST** /api/namespaces/{tenant}/members | Invite member| |[**apiKeyCreate**](#apikeycreate) | **POST** /api/namespaces/api-key | Creates an API key.| |[**apiKeyDelete**](#apikeydelete) | **DELETE** /api/namespaces/api-key/{key} | Delete an API key| |[**apiKeyList**](#apikeylist) | **GET** /api/namespaces/api-key | List API Keys| |[**apiKeyUpdate**](#apikeyupdate) | **PATCH** /api/namespaces/api-key/{key} | Update an API key| +|[**cancelMembershipInvitation**](#cancelmembershipinvitation) | **DELETE** /api/namespaces/{tenant}/invitations/{user-id} | Cancel a pending membership invitation| |[**connectorCreate**](#connectorcreate) | **POST** /api/connector | Connector\'s create| |[**connectorDelete**](#connectordelete) | **DELETE** /api/connector/{uid} | Connector\'s delete| |[**connectorGet**](#connectorget) | **GET** /api/connector/{uid} | Connector\'s get| @@ -18,14 +19,17 @@ All URIs are relative to *http://localhost* |[**connectorUpdate**](#connectorupdate) | **PATCH** /api/connector/{uid} | Connector\'s setting update| |[**createNamespace**](#createnamespace) | **POST** /api/namespaces | Create namespace| |[**createNamespaceAdmin**](#createnamespaceadmin) | **POST** /admin/api/namespaces/{tenant} | Create namespace admin| +|[**declineInvite**](#declineinvite) | **PATCH** /api/namespaces/{tenant}/invitations/decline | Decline a membership invite| |[**deleteNamespace**](#deletenamespace) | **DELETE** /api/namespaces/{tenant} | Delete namespace| |[**deleteNamespaceAdmin**](#deletenamespaceadmin) | **DELETE** /admin/api/namespaces/{tenant} | Delete namespace admin| |[**editNamespace**](#editnamespace) | **PUT** /api/namespaces/{tenant} | Edit namespace| |[**editNamespaceAdmin**](#editnamespaceadmin) | **PUT** /admin/api/namespaces-update/{tenantID} | Edit namespace admin| |[**exportNamespaces**](#exportnamespaces) | **GET** /admin/api/export/namespaces | export namespace| -|[**generateInvitationLink**](#generateinvitationlink) | **POST** /api/namespaces/{tenant}/members/invites | Generate an invitation link for a namespace member| +|[**generateInvitationLink**](#generateinvitationlink) | **POST** /api/namespaces/{tenant}/invitations/links | Generate an invitation link for a namespace member| +|[**getMembershipInvitationList**](#getmembershipinvitationlist) | **GET** /api/users/invitations | Get membership invitations for the authenticated user| |[**getNamespace**](#getnamespace) | **GET** /api/namespaces/{tenant} | Get a namespace| |[**getNamespaceAdmin**](#getnamespaceadmin) | **GET** /admin/api/namespaces/{tenant} | Get namespace admin| +|[**getNamespaceMembershipInvitationList**](#getnamespacemembershipinvitationlist) | **GET** /api/namespaces/{tenant}/invitations | Get membership invitations for a namespace| |[**getNamespaceSupport**](#getnamespacesupport) | **GET** /api/namespaces/{tenant}/support | Get a namespace support identifier.| |[**getNamespaceToken**](#getnamespacetoken) | **GET** /api/auth/token/{tenant} | Get a new namespace\'s token| |[**getNamespaces**](#getnamespaces) | **GET** /api/namespaces | Get namespaces list| @@ -34,31 +38,29 @@ All URIs are relative to *http://localhost* |[**lookupUserStatus**](#lookupuserstatus) | **GET** /api/namespaces/{tenant}/members/{id}/accept-invite | Lookup User\'s Status| |[**lookupUserStatus_0**](#lookupuserstatus_0) | **GET** /api/namespaces/{tenant}/members/{id}/accept-invite | Lookup User\'s Status| |[**removeNamespaceMember**](#removenamespacemember) | **DELETE** /api/namespaces/{tenant}/members/{uid} | Remove a member from a namespace| +|[**updateMembershipInvitation**](#updatemembershipinvitation) | **PATCH** /api/namespaces/{tenant}/invitations/{user-id} | Update a pending membership invitation| |[**updateNamespaceMember**](#updatenamespacemember) | **PATCH** /api/namespaces/{tenant}/members/{uid} | Update a member from a namespace| # **acceptInvite** > acceptInvite() -This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. +Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. ### Example ```typescript import { NamespacesApi, - Configuration, - AcceptInviteRequest + Configuration } from './api'; const configuration = new Configuration(); const apiInstance = new NamespacesApi(configuration); let tenant: string; //Namespace\'s tenant ID (default to undefined) -let acceptInviteRequest: AcceptInviteRequest; // (optional) const { status, data } = await apiInstance.acceptInvite( - tenant, - acceptInviteRequest + tenant ); ``` @@ -66,7 +68,6 @@ const { status, data } = await apiInstance.acceptInvite( |Name | Type | Description | Notes| |------------- | ------------- | ------------- | -------------| -| **acceptInviteRequest** | **AcceptInviteRequest**| | | | **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| @@ -80,7 +81,7 @@ void (empty response body) ### HTTP request headers - - **Content-Type**: application/json + - **Content-Type**: Not defined - **Accept**: application/json @@ -390,6 +391,65 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **cancelMembershipInvitation** +> cancelMembershipInvitation() + +Allows namespace administrators to cancel a pending membership invitation. The invitation status will be updated to \"cancelled\". The active user must have authority over the role of the invitation being cancelled. + +### Example + +```typescript +import { + NamespacesApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new NamespacesApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let userId: string; //The ID of the invited user (default to undefined) + +const { status, data } = await apiInstance.cancelMembershipInvitation( + tenant, + userId +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **userId** | [**string**] | The ID of the invited user | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully cancelled | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **connectorCreate** > connectorCreate(connectorData) @@ -849,6 +909,62 @@ const { status, data } = await apiInstance.createNamespaceAdmin( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **declineInvite** +> declineInvite() + +Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + +### Example + +```typescript +import { + NamespacesApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new NamespacesApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) + +const { status, data } = await apiInstance.declineInvite( + tenant +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully declined | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **deleteNamespace** > deleteNamespace() @@ -1199,6 +1315,65 @@ const { status, data } = await apiInstance.generateInvitationLink( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getMembershipInvitationList** +> Array getMembershipInvitationList() + +Returns a paginated list of membership invitations for the authenticated user. This endpoint allows users to view all namespace invitations they have received. + +### Example + +```typescript +import { + NamespacesApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new NamespacesApi(configuration); + +let filter: string; //Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. (optional) (default to undefined) +let page: number; //Page number (optional) (default to 1) +let perPage: number; //Items per page (optional) (default to 10) + +const { status, data } = await apiInstance.getMembershipInvitationList( + filter, + page, + perPage +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **filter** | [**string**] | Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. | (optional) defaults to undefined| +| **page** | [**number**] | Page number | (optional) defaults to 1| +| **perPage** | [**number**] | Items per page | (optional) defaults to 10| + + +### Return type + +**Array** + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successfully retrieved membership invitations list. | * X-Total-Count - Total number of membership invitations.
| +|**401** | Unauthorized | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **getNamespace** > Namespace getNamespace() @@ -1309,6 +1484,70 @@ const { status, data } = await apiInstance.getNamespaceAdmin( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getNamespaceMembershipInvitationList** +> Array getNamespaceMembershipInvitationList() + +Returns a paginated list of membership invitations for the specified namespace. This endpoint allows namespace administrators to view all pending invitations. + +### Example + +```typescript +import { + NamespacesApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new NamespacesApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let filter: string; //Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. (optional) (default to undefined) +let page: number; //Page number (optional) (default to 1) +let perPage: number; //Items per page (optional) (default to 10) + +const { status, data } = await apiInstance.getNamespaceMembershipInvitationList( + tenant, + filter, + page, + perPage +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **filter** | [**string**] | Membership invitations filter. Filter field receives a base64 encoded JSON object to limit the search. | (optional) defaults to undefined| +| **page** | [**number**] | Page number | (optional) defaults to 1| +| **perPage** | [**number**] | Items per page | (optional) defaults to 10| + + +### Return type + +**Array** + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successfully retrieved namespace membership invitations list. | * X-Total-Count - Total number of membership invitations.
| +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **getNamespaceSupport** > Support getNamespaceSupport() @@ -1771,6 +2010,69 @@ const { status, data } = await apiInstance.removeNamespaceMember( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **updateMembershipInvitation** +> updateMembershipInvitation() + +Allows namespace administrators to update a pending membership invitation. Currently supports updating the role assigned to the invitation. The active user must have authority over the role being assigned. + +### Example + +```typescript +import { + NamespacesApi, + Configuration, + UpdateNamespaceMemberRequest +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new NamespacesApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) +let userId: string; //The ID of the invited user (default to undefined) +let updateNamespaceMemberRequest: UpdateNamespaceMemberRequest; // (optional) + +const { status, data } = await apiInstance.updateMembershipInvitation( + tenant, + userId, + updateNamespaceMemberRequest +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **updateNamespaceMemberRequest** | **UpdateNamespaceMemberRequest**| | | +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| +| **userId** | [**string**] | The ID of the invited user | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully updated | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **updateNamespaceMember** > updateNamespaceMember() diff --git a/ui/src/api/client/docs/UsersApi.md b/ui/src/api/client/docs/UsersApi.md index f04433e7890..9f674d766bd 100644 --- a/ui/src/api/client/docs/UsersApi.md +++ b/ui/src/api/client/docs/UsersApi.md @@ -4,7 +4,7 @@ All URIs are relative to *http://localhost* |Method | HTTP request | Description| |------------- | ------------- | -------------| -|[**acceptInvite**](#acceptinvite) | **PATCH** /api/namespaces/{tenant}/members/accept-invite | Accept a membership invite| +|[**acceptInvite**](#acceptinvite) | **PATCH** /api/namespaces/{tenant}/invitations/accept | Accept a membership invite| |[**adminDeleteUser**](#admindeleteuser) | **DELETE** /admin/api/users/{id} | Delete user| |[**adminResetUserPassword**](#adminresetuserpassword) | **PATCH** /admin/api/users/{id}/password/reset | Reset user password| |[**adminUpdateUser**](#adminupdateuser) | **PUT** /admin/api/users/{id} | Update user| @@ -12,6 +12,7 @@ All URIs are relative to *http://localhost* |[**authUser**](#authuser) | **POST** /api/auth/user | Auth a user| |[**checkSessionRecord**](#checksessionrecord) | **GET** /api/users/security | Check session record status| |[**createUserAdmin**](#createuseradmin) | **POST** /admin/api/users | Create a User admin| +|[**declineInvite**](#declineinvite) | **PATCH** /api/namespaces/{tenant}/invitations/decline | Decline a membership invite| |[**deleteUser**](#deleteuser) | **DELETE** /api/user | Delete user| |[**disableMFA**](#disablemfa) | **PUT** /api/user/mfa/disable | Disable MFA| |[**enableMFA**](#enablemfa) | **PUT** /api/user/mfa/enable | Enable MFA| @@ -39,26 +40,23 @@ All URIs are relative to *http://localhost* # **acceptInvite** > acceptInvite() -This route is intended to be accessed directly through the link sent in the invitation email. The user must be logged into the account that was invited. +Accepts a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. ### Example ```typescript import { UsersApi, - Configuration, - AcceptInviteRequest + Configuration } from './api'; const configuration = new Configuration(); const apiInstance = new UsersApi(configuration); let tenant: string; //Namespace\'s tenant ID (default to undefined) -let acceptInviteRequest: AcceptInviteRequest; // (optional) const { status, data } = await apiInstance.acceptInvite( - tenant, - acceptInviteRequest + tenant ); ``` @@ -66,7 +64,6 @@ const { status, data } = await apiInstance.acceptInvite( |Name | Type | Description | Notes| |------------- | ------------- | ------------- | -------------| -| **acceptInviteRequest** | **AcceptInviteRequest**| | | | **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| @@ -80,7 +77,7 @@ void (empty response body) ### HTTP request headers - - **Content-Type**: application/json + - **Content-Type**: Not defined - **Accept**: application/json @@ -481,6 +478,62 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **declineInvite** +> declineInvite() + +Declines a pending membership invitation for the authenticated user. The user must be logged into the account that was invited. The invitation status will be updated to \"rejected\". + +### Example + +```typescript +import { + UsersApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new UsersApi(configuration); + +let tenant: string; //Namespace\'s tenant ID (default to undefined) + +const { status, data } = await apiInstance.declineInvite( + tenant +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tenant** | [**string**] | Namespace\'s tenant ID | defaults to undefined| + + +### Return type + +void (empty response body) + +### Authorization + +[jwt](../README.md#jwt) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Invitation successfully declined | - | +|**400** | Bad request | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**404** | Not found | - | +|**500** | Internal error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **deleteUser** > deleteUser() diff --git a/ui/src/components/AppBar/AppBar.vue b/ui/src/components/AppBar/AppBar.vue index bf38da3f8b9..04b69c65b66 100644 --- a/ui/src/components/AppBar/AppBar.vue +++ b/ui/src/components/AppBar/AppBar.vue @@ -59,7 +59,17 @@ Need assistance? Click here for support. - + + + supportStore.identifier); const isDarkMode = ref(theme.value === "dark"); const chatSupportPaywall = ref(false); const showNavigationDrawer = defineModel(); +const showDevicesDrawer = ref(false); +const showInvitationsDrawer = ref(false); const triggerClick = async (item: MenuItem) => { switch (item.type) { @@ -301,15 +315,15 @@ const redirectToGitHub = (): void => { const openShellhubHelp = async (): Promise => { switch (true) { - case envVariables.isCloud && isBillingActive.value: + case isCloud && isBillingActive.value: await openChatwoot(); break; - case envVariables.isCommunity || (envVariables.isCloud && !isBillingActive.value): + case isCommunity || (isCloud && !isBillingActive.value): openPaywall(); break; - case envVariables.isEnterprise: + case isEnterprise: redirectToGitHub(); break; diff --git a/ui/src/components/AppBar/DevicesDropdown.vue b/ui/src/components/AppBar/DevicesDropdown.vue index 7a291d35a16..90ae059b0f2 100644 --- a/ui/src/components/AppBar/DevicesDropdown.vue +++ b/ui/src/components/AppBar/DevicesDropdown.vue @@ -325,7 +325,7 @@ const { smAndUp, thresholds } = useDisplay(); const devicesStore = useDevicesStore(); const snackbar = useSnackbar(); -const isDrawerOpen = ref(false); +const isDrawerOpen = defineModel({ required: true }); const activeTab = ref<"pending" | "recent">("pending"); const totalDevices = computed(() => devicesStore.totalDevicesCount); diff --git a/ui/src/components/Invitations/InvitationAccept.vue b/ui/src/components/Invitations/InvitationAccept.vue new file mode 100644 index 00000000000..c5b17436606 --- /dev/null +++ b/ui/src/components/Invitations/InvitationAccept.vue @@ -0,0 +1,69 @@ + + + diff --git a/ui/src/components/Invitations/InvitationDecline.vue b/ui/src/components/Invitations/InvitationDecline.vue new file mode 100644 index 00000000000..2b2bf219e9c --- /dev/null +++ b/ui/src/components/Invitations/InvitationDecline.vue @@ -0,0 +1,61 @@ + + + diff --git a/ui/src/components/Invitations/InvitationsMenu.vue b/ui/src/components/Invitations/InvitationsMenu.vue new file mode 100644 index 00000000000..05385d2da52 --- /dev/null +++ b/ui/src/components/Invitations/InvitationsMenu.vue @@ -0,0 +1,128 @@ + + + diff --git a/ui/src/components/Invitations/InvitationsMenuItem.vue b/ui/src/components/Invitations/InvitationsMenuItem.vue new file mode 100644 index 00000000000..64a954956db --- /dev/null +++ b/ui/src/components/Invitations/InvitationsMenuItem.vue @@ -0,0 +1,95 @@ + + + diff --git a/ui/src/components/Team/Invitation/InvitationCancel.vue b/ui/src/components/Team/Invitation/InvitationCancel.vue new file mode 100644 index 00000000000..00d9a886f7a --- /dev/null +++ b/ui/src/components/Team/Invitation/InvitationCancel.vue @@ -0,0 +1,100 @@ + + + diff --git a/ui/src/components/Team/Invitation/InvitationEdit.vue b/ui/src/components/Team/Invitation/InvitationEdit.vue new file mode 100644 index 00000000000..439bd8dfd26 --- /dev/null +++ b/ui/src/components/Team/Invitation/InvitationEdit.vue @@ -0,0 +1,107 @@ + + + diff --git a/ui/src/components/Team/Invitation/InvitationList.vue b/ui/src/components/Team/Invitation/InvitationList.vue new file mode 100644 index 00000000000..237aadb6d0d --- /dev/null +++ b/ui/src/components/Team/Invitation/InvitationList.vue @@ -0,0 +1,257 @@ + + + diff --git a/ui/src/components/Team/Invitation/InvitationResend.vue b/ui/src/components/Team/Invitation/InvitationResend.vue new file mode 100644 index 00000000000..e9fcb7142d3 --- /dev/null +++ b/ui/src/components/Team/Invitation/InvitationResend.vue @@ -0,0 +1,113 @@ + + + diff --git a/ui/src/components/Team/Member/MemberInvite.vue b/ui/src/components/Team/Member/MemberInvite.vue index 03127f23c4d..38766ae8aaa 100644 --- a/ui/src/components/Team/Member/MemberInvite.vue +++ b/ui/src/components/Team/Member/MemberInvite.vue @@ -114,11 +114,11 @@ import CopyWarning from "@/components/User/CopyWarning.vue"; import RoleSelect from "../RoleSelect.vue"; import { BasicRole } from "@/interfaces/INamespace"; import useAuthStore from "@/store/modules/auth"; -import useNamespacesStore from "@/store/modules/namespaces"; +import useInvitationsStore from "@/store/modules/invitations"; const emit = defineEmits(["update"]); const authStore = useAuthStore(); -const namespacesStore = useNamespacesStore(); +const invitationsStore = useInvitationsStore(); const snackbar = useSnackbar(); const showDialog = ref(false); const isLoading = ref(false); @@ -149,11 +149,6 @@ const close = () => { formWindow.value = "form-1"; }; -const update = () => { - emit("update"); - close(); -}; - const handleInviteError = (error: unknown) => { snackbar.showError("Failed to send invitation."); @@ -182,9 +177,10 @@ const getInvitePayload = () => ({ const generateLinkInvite = async () => { isLoading.value = true; try { - invitationLink.value = await namespacesStore.generateInvitationLink(getInvitePayload()); + invitationLink.value = await invitationsStore.generateInvitationLink(getInvitePayload()); snackbar.showSuccess("Invitation link generated successfully."); formWindow.value = "form-2"; + emit("update"); } catch (error) { handleInviteError(error); } finally { @@ -195,10 +191,10 @@ const generateLinkInvite = async () => { const sendEmailInvite = async () => { isLoading.value = true; try { - await namespacesStore.sendEmailInvitation(getInvitePayload()); + await invitationsStore.sendInvitationEmail(getInvitePayload()); snackbar.showSuccess("Invitation email sent successfully."); - update(); - resetFields(); + emit("update"); + close(); } catch (error) { handleInviteError(error); } finally { diff --git a/ui/src/components/Team/Member/MemberList.vue b/ui/src/components/Team/Member/MemberList.vue index 0056e315548..47875370c7a 100644 --- a/ui/src/components/Team/Member/MemberList.vue +++ b/ui/src/components/Team/Member/MemberList.vue @@ -41,14 +41,6 @@ mdi-account {{ member.email }} - - - - {{ member.role }} - - - - {{ member.status }} + + {{ member.role }} + + ; } export interface INamespaceEditMember { user_id: string; - role: Role; + role: BasicRole; tenant_id: string; } @@ -61,9 +55,3 @@ export interface INamespaceRemoveMember { tenant_id: string; user_id: string; } - -export interface INamespaceEdit { - tenant_id: string; - name?: string; - settings?: Partial; -} diff --git a/ui/src/router/index.ts b/ui/src/router/index.ts index 06f9aa77d06..075c2a7a63e 100755 --- a/ui/src/router/index.ts +++ b/ui/src/router/index.ts @@ -10,6 +10,7 @@ import useUsersStore from "@/store/modules/users"; import useWebEndpointsStore from "@/store/modules/web_endpoints"; import { computed } from "vue"; import useTagsStore from "@/store/modules/tags"; +import hasPermission from "@/utils/permission"; export const handleAcceptInvite = async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => { const namespacesStore = useNamespacesStore(); @@ -81,6 +82,7 @@ const SettingPrivateKeys = () => import("@/components/Setting/SettingPrivateKeys const SettingBilling = () => import("@/components/Setting/SettingBilling.vue"); const TeamMembers = () => import("@/views/TeamMembers.vue"); const TeamApiKeys = () => import("@/views/TeamApiKeys.vue"); +const TeamInvitations = () => import("@/views/TeamInvitations.vue"); export const routes: Array = [ { @@ -479,6 +481,16 @@ export const routes: Array = [ showInSidebar: true, }, }, + { + path: "invitations", + name: "Invitations", + component: TeamInvitations, + meta: { + title: "Invitations", + showInSidebar: true, + isHidden: () => !envVariables.isCloud || !hasPermission("namespace:editInvitation"), + }, + }, ], }, { diff --git a/ui/src/store/api/invitations.ts b/ui/src/store/api/invitations.ts new file mode 100644 index 00000000000..8c54a34f593 --- /dev/null +++ b/ui/src/store/api/invitations.ts @@ -0,0 +1,31 @@ +import { namespacesApi } from "@/api/http"; +import { IInviteMemberPayload } from "@/interfaces/IInvitation"; +import { BasicRole } from "@/interfaces/INamespace"; + +export const fetchUserPendingInvitations = async (data: { filter: string, page: number, perPage: number }) => + namespacesApi.getMembershipInvitationList(data.filter, data.page, data.perPage); + +export const fetchNamespaceInvitations = async (tenantId: string, data: { filter?: string, page: number, perPage: number }) => + namespacesApi.getNamespaceMembershipInvitationList(tenantId, data.filter, data.page, data.perPage); + +export const declineNamespaceInvitation = async (tenant: string) => + namespacesApi.declineInvite(tenant); + +export const acceptNamespaceInvitation = async (tenant: string) => + namespacesApi.acceptInvite(tenant); + +export const editNamespaceInvitation = async (data: { tenant: string; user_id: string; role: BasicRole }) => + namespacesApi.updateMembershipInvitation(data.tenant, data.user_id, { role: data.role }); + +export const cancelNamespaceInvitation = async (data: { tenant: string; user_id: string }) => + namespacesApi.cancelMembershipInvitation(data.tenant, data.user_id); + +export const sendNamespaceInvitationEmail = async (data: IInviteMemberPayload) => namespacesApi.addNamespaceMember(data.tenant_id, { + email: data.email, + role: data.role, +}); + +export const generateNamespaceInvitationLink = async (data: IInviteMemberPayload) => namespacesApi.generateInvitationLink(data.tenant_id, { + email: data.email, + role: data.role, +}); diff --git a/ui/src/store/api/namespaces.ts b/ui/src/store/api/namespaces.ts index 0bea57b543a..c9609d3fdfe 100755 --- a/ui/src/store/api/namespaces.ts +++ b/ui/src/store/api/namespaces.ts @@ -1,10 +1,4 @@ -import { - INamespaceAcceptInvite, - INamespaceAddMember, - INamespaceEdit, - INamespaceEditMember, - INamespaceRemoveMember, -} from "@/interfaces/INamespace"; +import { INamespaceEdit, INamespaceEditMember, INamespaceRemoveMember } from "@/interfaces/INamespace"; import { namespacesApi } from "@/api/http"; export const createNamespace = async (name: string) => namespacesApi.createNamespace({ name }); @@ -25,16 +19,6 @@ export const editNamespace = async (data: INamespaceEdit) => namespacesApi.editN }, }); -export const sendNamespaceLink = async (data: INamespaceAddMember) => namespacesApi.addNamespaceMember(data.tenant_id, { - email: data.email, - role: data.role, -}); - -export const generateNamespaceLink = async (data: INamespaceAddMember) => namespacesApi.generateInvitationLink(data.tenant_id, { - email: data.email, - role: data.role, -}); - export const updateNamespaceMember = async (data: INamespaceEditMember) => namespacesApi.updateNamespaceMember( data.tenant_id, data.user_id, @@ -48,8 +32,6 @@ export const removeUserFromNamespace = async (data: INamespaceRemoveMember) => n export const switchNamespace = async (tenantId: string) => namespacesApi.getNamespaceToken(tenantId); -export const acceptNamespaceInvite = async (data: INamespaceAcceptInvite) => namespacesApi.acceptInvite(data.tenant, { sig: data.sig }); - export const getSupportID = async (tenant: string) => namespacesApi.getNamespaceSupport(tenant); export const lookupUserStatus = async ( diff --git a/ui/src/store/modules/invitations.ts b/ui/src/store/modules/invitations.ts new file mode 100644 index 00000000000..42eb10d4c86 --- /dev/null +++ b/ui/src/store/modules/invitations.ts @@ -0,0 +1,79 @@ +import { ref } from "vue"; +import { defineStore } from "pinia"; +import { IInvitation, IInviteMemberPayload } from "@/interfaces/IInvitation"; +import * as invitationsApi from "../api/invitations"; +import { BasicRole } from "@/interfaces/INamespace"; +import { getInvitationStatusFilter } from "@/utils/invitations"; + +const useInvitationsStore = defineStore("invitations", () => { + const pendingInvitations = ref([]); + const namespaceInvitations = ref([]); + const invitationCount = ref(0); + + const pendingInvitesFilter = getInvitationStatusFilter("pending"); + + const fetchUserPendingInvitationList = async () => { + const res = await invitationsApi.fetchUserPendingInvitations({ + filter: pendingInvitesFilter, + page: 1, + perPage: 100, + }); + pendingInvitations.value = res.data as IInvitation[]; + }; + + const fetchNamespaceInvitationList = async ( + tenantId: string, + page: number, + perPage: number, + filter?: string, + ) => { + const res = await invitationsApi.fetchNamespaceInvitations(tenantId, { + filter, + page, + perPage, + }); + namespaceInvitations.value = res.data as IInvitation[]; + invitationCount.value = Number(res.headers["x-total-count"]); + }; + + const acceptInvitation = async (tenant: string) => { + await invitationsApi.acceptNamespaceInvitation(tenant); + }; + + const declineInvitation = async (tenant: string) => { + await invitationsApi.declineNamespaceInvitation(tenant); + }; + + const editInvitation = async (data: { tenant: string; user_id: string; role: BasicRole }) => { + await invitationsApi.editNamespaceInvitation(data); + }; + + const cancelInvitation = async (data: { tenant: string; user_id: string }) => { + await invitationsApi.cancelNamespaceInvitation(data); + }; + + const sendInvitationEmail = async (data: IInviteMemberPayload) => { + await invitationsApi.sendNamespaceInvitationEmail(data); + }; + + const generateInvitationLink = async (data: IInviteMemberPayload) => { + const response = await invitationsApi.generateNamespaceInvitationLink(data); + return response.data.link as string; + }; + + return { + pendingInvitations, + namespaceInvitations, + invitationCount, + fetchUserPendingInvitationList, + fetchNamespaceInvitationList, + acceptInvitation, + declineInvitation, + editInvitation, + cancelInvitation, + sendInvitationEmail, + generateInvitationLink, + }; +}); + +export default useInvitationsStore; diff --git a/ui/src/store/modules/namespaces.ts b/ui/src/store/modules/namespaces.ts index 812e403fe70..0cf47cfc14b 100755 --- a/ui/src/store/modules/namespaces.ts +++ b/ui/src/store/modules/namespaces.ts @@ -3,8 +3,6 @@ import { ref } from "vue"; import * as namespacesApi from "../api/namespaces"; import { INamespace, - INamespaceAcceptInvite, - INamespaceAddMember, INamespaceEdit, INamespaceEditMember, INamespaceRemoveMember, @@ -52,15 +50,6 @@ const useNamespacesStore = defineStore("namespaces", () => { } }; - const sendEmailInvitation = async (data: INamespaceAddMember) => { - await namespacesApi.sendNamespaceLink(data); - }; - - const generateInvitationLink = async (data: INamespaceAddMember) => { - const res = await namespacesApi.generateNamespaceLink(data); - return res.data.link as string; - }; - const updateNamespaceMember = async (data: INamespaceEditMember) => { await namespacesApi.updateNamespaceMember(data); }; @@ -69,10 +58,6 @@ const useNamespacesStore = defineStore("namespaces", () => { await namespacesApi.removeUserFromNamespace(data); }; - const acceptInvite = async (data: INamespaceAcceptInvite) => { - await namespacesApi.acceptNamespaceInvite(data); - }; - const lookupUserStatus = async (data: { tenant: string; id: string; sig: string; }) => { const res = await namespacesApi.lookupUserStatus(data); userStatus.value = res.data.status; @@ -106,11 +91,8 @@ const useNamespacesStore = defineStore("namespaces", () => { editNamespace, deleteNamespace, leaveNamespace, - sendEmailInvitation, - generateInvitationLink, updateNamespaceMember, removeMemberFromNamespace, - acceptInvite, lookupUserStatus, switchNamespace, reset, diff --git a/ui/src/utils/invitations.ts b/ui/src/utils/invitations.ts new file mode 100644 index 00000000000..486d8be1c66 --- /dev/null +++ b/ui/src/utils/invitations.ts @@ -0,0 +1,19 @@ +import { IInvitation } from "@/interfaces/IInvitation"; + +export const getInvitationStatusFilter = (status: IInvitation["status"]) => { + const filter = [{ + type: "property", + params: { name: "status", operator: "eq", value: status }, + }]; + + return Buffer.from(JSON.stringify(filter)).toString("base64"); +}; + +export const orderInvitationsByCreatedAt = (invitations: IInvitation[]) => { + return invitations.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); +}; + +export const isInvitationExpired = (expiresAt: IInvitation["expires_at"]): boolean => { + if (!expiresAt) return false; + return new Date(expiresAt).getTime() < Date.now(); +}; diff --git a/ui/src/utils/permission.ts b/ui/src/utils/permission.ts index 5cc77360423..7e526868ed0 100644 --- a/ui/src/utils/permission.ts +++ b/ui/src/utils/permission.ts @@ -28,6 +28,8 @@ const permissions = { "namespace:addMember": Roles.ADMINISTRATOR, "namespace:editMember": Roles.ADMINISTRATOR, "namespace:removeMember": Roles.ADMINISTRATOR, + "namespace:editInvitation": Roles.ADMINISTRATOR, + "namespace:cancelInvitation": Roles.ADMINISTRATOR, "namespace:updateSessionRecording": Roles.ADMINISTRATOR, "namespace:delete": Roles.OWNER, diff --git a/ui/src/views/NamespaceInviteCard.vue b/ui/src/views/NamespaceInviteCard.vue index e4c846c3289..bc6451764a3 100644 --- a/ui/src/views/NamespaceInviteCard.vue +++ b/ui/src/views/NamespaceInviteCard.vue @@ -4,118 +4,88 @@ type="error" variant="tonal" data-test="error-alert" - > - {{ errorAlert }} - + :text="errorAlert" + /> - {{ title }} + Namespace Invitation -
{{ message }} -
+ - - + + + - Accept Invitation - + + diff --git a/ui/src/views/TeamInvitations.vue b/ui/src/views/TeamInvitations.vue new file mode 100644 index 00000000000..365cc988874 --- /dev/null +++ b/ui/src/views/TeamInvitations.vue @@ -0,0 +1,55 @@ + + + diff --git a/ui/tests/components/AppBar/AppBar.spec.ts b/ui/tests/components/AppBar/AppBar.spec.ts index 41e562461ad..2960732ecd5 100644 --- a/ui/tests/components/AppBar/AppBar.spec.ts +++ b/ui/tests/components/AppBar/AppBar.spec.ts @@ -65,6 +65,9 @@ const systemInfo = { }, }; +// eslint-disable-next-line vue/max-len +const mockInvitationsUrl = "http://localhost:3000/api/users/invitations?filter=W3sidHlwZSI6InByb3BlcnR5IiwicGFyYW1zIjp7Im5hbWUiOiJzdGF0dXMiLCJvcGVyYXRvciI6ImVxIiwidmFsdWUiOiJwZW5kaW5nIn19XQ%3D%3D&page=1&per_page=100"; + describe("AppBar Component", () => { let wrapper: VueWrapper; const vuetify = createVuetify(); @@ -93,6 +96,7 @@ describe("AppBar Component", () => { mockDevicesApi.onGet("http://localhost:3000/api/devices?page=1&per_page=10&status=accepted").reply(200, []); mockDevicesApi.onGet("http://localhost:3000/api/stats").reply(200, {}); mockNamespacesApi.onGet("http://localhost:3000/api/namespaces?page=1&per_page=30").reply(200, []); + mockNamespacesApi.onGet(mockInvitationsUrl).reply(200, []); authStore.$patch(authStoreData); billingStore.billing = billingData; @@ -165,6 +169,18 @@ describe("AppBar Component", () => { it("Opens the paywall if instance is community", async () => { envVariables.isCloud = false; envVariables.isCommunity = true; + wrapper.unmount(); + wrapper = mount(Component, { + global: { + plugins: [vuetify, router, SnackbarPlugin], + components: { + "v-layout": VLayout, + AppBar, + }, + }, + }); + + await flushPromises(); const drawer = wrapper.findComponent(AppBar); await drawer.vm.openShellhubHelp(); diff --git a/ui/tests/components/AppBar/DevicesDropdown.spec.ts b/ui/tests/components/AppBar/DevicesDropdown.spec.ts index c3c339e5a16..3852606f757 100644 --- a/ui/tests/components/AppBar/DevicesDropdown.spec.ts +++ b/ui/tests/components/AppBar/DevicesDropdown.spec.ts @@ -18,7 +18,11 @@ import useNamespacesStore from "@/store/modules/namespaces"; import { INamespace, INamespaceMember } from "@/interfaces/INamespace"; const Component = { - template: "", + template: "", + props: ["modelValue"], + data: () => ({ + show: true, + }), }; // Mock Vuetify display @@ -191,6 +195,9 @@ describe("Device Management Dropdown", () => { }, stubs: { teleport: true }, }, + props: { + modelValue: true, + }, attachTo: document.body, }); @@ -214,6 +221,9 @@ describe("Device Management Dropdown", () => { plugins: [vuetify, router, SnackbarPlugin], components: { "v-layout": VLayout, DevicesDropdown }, }, + props: { + modelValue: true, + }, }); await flushPromises(); diff --git a/ui/tests/components/AppBar/__snapshots__/AppBar.spec.ts.snap b/ui/tests/components/AppBar/__snapshots__/AppBar.spec.ts.snap index 2d16a38d47a..444f84d6b7b 100644 --- a/ui/tests/components/AppBar/__snapshots__/AppBar.spec.ts.snap +++ b/ui/tests/components/AppBar/__snapshots__/AppBar.spec.ts.snap @@ -4,7 +4,7 @@ exports[`AppBar Component > Renders the component 1`] = ` "
-
+
@@ -43,7 +43,14 @@ exports[`AppBar Component > Renders the component 1`] = ` - 1 of 1
+
+ + +" +`; diff --git a/ui/tests/components/Team/Member/MemberInvite.spec.ts b/ui/tests/components/Team/Member/MemberInvite.spec.ts index d0fd7f6c17e..d3eac59c931 100644 --- a/ui/tests/components/Team/Member/MemberInvite.spec.ts +++ b/ui/tests/components/Team/Member/MemberInvite.spec.ts @@ -9,7 +9,7 @@ import { router } from "@/router"; import { SnackbarInjectionKey } from "@/plugins/snackbar"; import useAuthStore from "@/store/modules/auth"; import { envVariables } from "@/envVariables"; -import useNamespacesStore from "@/store/modules/namespaces"; +import useInvitationsStore from "@/store/modules/invitations"; type MemberInviteWrapper = VueWrapper>; @@ -23,7 +23,7 @@ describe("Member Invite", () => { let wrapper: MemberInviteWrapper; setActivePinia(createPinia()); const authStore = useAuthStore(); - const namespacesStore = useNamespacesStore(); + const invitationsStore = useInvitationsStore(); const vuetify = createVuetify(); const mockNamespacesApi = new MockAdapter(namespacesApi.getAxios()); @@ -55,7 +55,7 @@ describe("Member Invite", () => { it("Invite Member Email - Error Validation", async () => { mockNamespacesApi.onPost("http://localhost:3000/api/namespaces/fake-tenant-data/members").reply(409); - const storeSpy = vi.spyOn(namespacesStore, "sendEmailInvitation"); + const storeSpy = vi.spyOn(invitationsStore, "sendInvitationEmail"); await wrapper.findComponent('[data-test="invite-dialog-btn"]').trigger("click"); @@ -79,7 +79,7 @@ describe("Member Invite", () => { it("Invite Member Email - Success Validation", async () => { mockNamespacesApi.onPost("http://localhost:3000/api/namespaces/fake-tenant-data/members").reply(200); - const storeSpy = vi.spyOn(namespacesStore, "sendEmailInvitation"); + const storeSpy = vi.spyOn(invitationsStore, "sendInvitationEmail"); await wrapper.findComponent('[data-test="invite-dialog-btn"]').trigger("click"); @@ -103,9 +103,9 @@ describe("Member Invite", () => { }); it("Generates Invitation Link - Failure", async () => { - mockNamespacesApi.onPost("http://localhost:3000/api/namespaces/fake-tenant-data/members/invites").reply(404); + mockNamespacesApi.onPost("http://localhost:3000/api/namespaces/fake-tenant-data/invitations/links").reply(404); - const storeSpy = vi.spyOn(namespacesStore, "generateInvitationLink"); + const storeSpy = vi.spyOn(invitationsStore, "generateInvitationLink"); await wrapper.findComponent('[data-test="invite-dialog-btn"]').trigger("click"); @@ -129,11 +129,11 @@ describe("Member Invite", () => { }); it("Generates Invitation Link - Success", async () => { - mockNamespacesApi.onPost("http://localhost:3000/api/namespaces/fake-tenant-data/members/invites").reply(200, { + mockNamespacesApi.onPost("http://localhost:3000/api/namespaces/fake-tenant-data/invitations/links").reply(200, { link: "http://localhost/invite-link", }); - const storeSpy = vi.spyOn(namespacesStore, "generateInvitationLink"); + const storeSpy = vi.spyOn(invitationsStore, "generateInvitationLink"); await wrapper.findComponent('[data-test="invite-dialog-btn"]').trigger("click"); @@ -152,9 +152,7 @@ describe("Member Invite", () => { tenant_id: "fake-tenant-data", role: "administrator", }); - expect(wrapper.vm.formWindow).toEqual("form-2"); - expect(wrapper.vm.invitationLink).toEqual("http://localhost/invite-link"); }); }); diff --git a/ui/tests/components/Team/Member/MemberList.spec.ts b/ui/tests/components/Team/Member/MemberList.spec.ts index dc04dd26d3a..4151a92d187 100644 --- a/ui/tests/components/Team/Member/MemberList.spec.ts +++ b/ui/tests/components/Team/Member/MemberList.spec.ts @@ -29,7 +29,6 @@ describe("Member List", () => { email: "test@test.com", role: "owner" as const, added_at: "2024-01-01T12:00:00Z", - status: "accepted" as const, }, ], settings: { diff --git a/ui/tests/layouts/__snapshots__/AppLayout.spec.ts.snap b/ui/tests/layouts/__snapshots__/AppLayout.spec.ts.snap index ab08b861444..e90699d481a 100644 --- a/ui/tests/layouts/__snapshots__/AppLayout.spec.ts.snap +++ b/ui/tests/layouts/__snapshots__/AppLayout.spec.ts.snap @@ -3,7 +3,7 @@ exports[`App Layout Component > Renders the component 1`] = ` "
-