diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs index 67b5f0da80fa..d8a3d763b4ed 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs @@ -2,9 +2,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Data; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -79,19 +77,10 @@ private async Task CreateDefaultCollectionsAsync(AutomaticallyConfirmOrganizatio return; } - await collectionRepository.CreateAsync( - new Collection - { - OrganizationId = request.Organization!.Id, - Name = request.DefaultUserCollectionName, - Type = CollectionType.DefaultUserCollection - }, - groups: null, - [new CollectionAccessSelection - { - Id = request.OrganizationUser!.Id, - Manage = true - }]); + await collectionRepository.UpsertDefaultCollectionAsync( + request.Organization!.Id, + request.OrganizationUser!.Id, + request.DefaultUserCollectionName); } catch (Exception ex) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 2fbe6be5c6c9..c703132e5a4a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -11,7 +11,6 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -274,21 +273,10 @@ private async Task CreateDefaultCollectionAsync(OrganizationUser organizationUse return; } - var defaultCollection = new Collection - { - OrganizationId = organizationUser.OrganizationId, - Name = defaultUserCollectionName, - Type = CollectionType.DefaultUserCollection - }; - var collectionUser = new CollectionAccessSelection - { - Id = organizationUser.Id, - ReadOnly = false, - HidePasswords = false, - Manage = true - }; - - await _collectionRepository.CreateAsync(defaultCollection, groups: null, users: [collectionUser]); + await _collectionRepository.UpsertDefaultCollectionAsync( + organizationUser.OrganizationId, + organizationUser.Id, + defaultUserCollectionName); } /// diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index f86147ca7da6..4e0af70729aa 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -71,4 +71,14 @@ Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable col /// The encrypted string to use as the default collection name. /// Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable organizationUserIds, string defaultCollectionName); + + /// + /// Creates a default user collection for the specified organization user if they do not already have one. + /// This operation is idempotent - calling it multiple times will not create duplicate collections. + /// + /// The Organization ID. + /// The Organization User ID to create/find a default collection for. + /// The encrypted string to use as the default collection name. + /// True if a new collection was created; false if the user already had a default collection. + Task UpsertDefaultCollectionAsync(Guid organizationId, Guid organizationUserId, string defaultCollectionName); } diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs index c2a59f75aa1c..d2e15670e299 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs @@ -363,6 +363,30 @@ public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable } } + public async Task UpsertDefaultCollectionAsync(Guid organizationId, Guid organizationUserId, string defaultCollectionName) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var collectionId = CoreHelpers.GenerateComb(); + var now = DateTime.UtcNow; + var parameters = new DynamicParameters(); + parameters.Add("@CollectionId", collectionId); + parameters.Add("@OrganizationId", organizationId); + parameters.Add("@OrganizationUserId", organizationUserId); + parameters.Add("@Name", defaultCollectionName); + parameters.Add("@CreationDate", now); + parameters.Add("@RevisionDate", now); + parameters.Add("@WasCreated", dbType: DbType.Boolean, direction: ParameterDirection.Output); + + await connection.ExecuteAsync( + $"[{Schema}].[Collection_UpsertDefaultCollection]", + parameters, + commandType: CommandType.StoredProcedure); + + return parameters.Get("@WasCreated"); + } + } + private async Task> GetOrgUserIdsWithDefaultCollectionAsync(SqlConnection connection, SqlTransaction transaction, Guid organizationId) { const string sql = @" diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 3c35df2a82fb..ae09fb1841f5 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -47,22 +47,34 @@ public static void SetupEntityFramework(this IServiceCollection services, string { if (provider == SupportedDatabaseProviders.Postgres) { - options.UseNpgsql(connectionString, b => b.MigrationsAssembly("PostgresMigrations")); + options.UseNpgsql(connectionString, b => + { + b.MigrationsAssembly("PostgresMigrations"); + b.EnableRetryOnFailure(); + }); // Handle NpgSql Legacy Support for `timestamp without timezone` issue AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); } else if (provider == SupportedDatabaseProviders.MySql) { options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), - b => b.MigrationsAssembly("MySqlMigrations")); + b => + { + b.MigrationsAssembly("MySqlMigrations"); + b.EnableRetryOnFailure(); + }); } else if (provider == SupportedDatabaseProviders.Sqlite) { + // SQLite doesn't support EnableRetryOnFailure options.UseSqlite(connectionString, b => b.MigrationsAssembly("SqliteMigrations")); } else if (provider == SupportedDatabaseProviders.SqlServer) { - options.UseSqlServer(connectionString); + options.UseSqlServer(connectionString, b => + { + b.EnableRetryOnFailure(); + }); } }); } diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 5aa156d1f821..5f1c6f00d89b 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -821,6 +821,83 @@ public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable await dbContext.SaveChangesAsync(); } + public async Task UpsertDefaultCollectionAsync(Guid organizationId, Guid organizationUserId, string defaultCollectionName) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + // Use EF's execution strategy to handle transient failures (including deadlocks) + var strategy = dbContext.Database.CreateExecutionStrategy(); + + return await strategy.ExecuteAsync(async () => + { + // Use SERIALIZABLE isolation level to prevent race conditions during concurrent calls + using var transaction = await dbContext.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable); + + try + { + // Check if this organization user already has a default collection + // SERIALIZABLE ensures this SELECT acquires range locks + var existingDefaultCollection = await ( + from c in dbContext.Collections + join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId + where cu.OrganizationUserId == organizationUserId + && c.OrganizationId == organizationId + && c.Type == CollectionType.DefaultUserCollection + select c + ).FirstOrDefaultAsync(); + + // If collection already exists, return false (not created) + if (existingDefaultCollection != null) + { + await transaction.CommitAsync(); + return false; + } + + // Create new default collection + var collectionId = CoreHelpers.GenerateComb(); + var now = DateTime.UtcNow; + + var collection = new Collection + { + Id = collectionId, + OrganizationId = organizationId, + Name = defaultCollectionName, + ExternalId = null, + CreationDate = now, + RevisionDate = now, + Type = CollectionType.DefaultUserCollection, + DefaultUserCollectionEmail = null + }; + + var collectionUser = new CollectionUser + { + CollectionId = collectionId, + OrganizationUserId = organizationUserId, + ReadOnly = false, + HidePasswords = false, + Manage = true + }; + + await dbContext.Collections.AddAsync(collection); + await dbContext.CollectionUsers.AddAsync(collectionUser); + await dbContext.SaveChangesAsync(); + + // Bump user account revision dates + await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(collectionId, organizationId); + await dbContext.SaveChangesAsync(); + + await transaction.CommitAsync(); + return true; + } + catch + { + await transaction.RollbackAsync(); + throw; + } + }); + } + private async Task> GetOrgUserIdsWithDefaultCollectionAsync(DatabaseContext dbContext, Guid organizationId) { var results = await dbContext.OrganizationUsers diff --git a/src/Sql/dbo/Stored Procedures/Collection_UpsertDefaultCollection.sql b/src/Sql/dbo/Stored Procedures/Collection_UpsertDefaultCollection.sql new file mode 100644 index 000000000000..7464cc2345df --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_UpsertDefaultCollection.sql @@ -0,0 +1,96 @@ +-- This procedure prevents duplicate "My Items" collections for users by checking +-- if a default collection already exists before attempting to create one. + +CREATE PROCEDURE [dbo].[Collection_UpsertDefaultCollection] + @CollectionId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @OrganizationUserId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @WasCreated BIT OUTPUT +AS +BEGIN + SET NOCOUNT ON + + -- Use SERIALIZABLE isolation level to prevent race conditions during concurrent calls + SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; + BEGIN TRANSACTION; + + BEGIN TRY + DECLARE @ExistingCollectionId UNIQUEIDENTIFIER; + + -- Check if this organization user already has a default collection + -- SERIALIZABLE ensures range locks prevent concurrent insertions + SELECT @ExistingCollectionId = c.Id + FROM [dbo].[Collection] c + INNER JOIN [dbo].[CollectionUser] cu ON cu.CollectionId = c.Id + WHERE cu.OrganizationUserId = @OrganizationUserId + AND c.OrganizationId = @OrganizationId + AND c.Type = 1; -- CollectionType.DefaultUserCollection + + -- If collection already exists, return early + IF @ExistingCollectionId IS NOT NULL + BEGIN + SET @WasCreated = 0; + COMMIT TRANSACTION; + RETURN; + END + + -- Create new default collection + SET @WasCreated = 1; + + -- Insert Collection + INSERT INTO [dbo].[Collection] + ( + [Id], + [OrganizationId], + [Name], + [ExternalId], + [CreationDate], + [RevisionDate], + [DefaultUserCollectionEmail], + [Type] + ) + VALUES + ( + @CollectionId, + @OrganizationId, + @Name, + NULL, -- ExternalId + @CreationDate, + @RevisionDate, + NULL, -- DefaultUserCollectionEmail + 1 -- CollectionType.DefaultUserCollection + ); + + -- Insert CollectionUser + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + VALUES + ( + @CollectionId, + @OrganizationUserId, + 0, -- ReadOnly = false + 0, -- HidePasswords = false + 1 -- Manage = true + ); + + -- Bump user account revision dates + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @CollectionId, @OrganizationId; + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH +END +GO diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs index 1035d5c578bb..f1b3dc6ec128 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs @@ -9,7 +9,6 @@ using Bit.Core.AdminConsole.Utilities.v2.Validation; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Data; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -203,14 +202,10 @@ public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectio await sutProvider.GetDependency() .Received(1) - .CreateAsync( - Arg.Is(c => - c.OrganizationId == organization.Id && - c.Name == defaultCollectionName && - c.Type == CollectionType.DefaultUserCollection), - Arg.Is>(groups => groups == null), - Arg.Is>(access => - access.FirstOrDefault(x => x.Id == organizationUser.Id && x.Manage) != null)); + .UpsertDefaultCollectionAsync( + organization.Id, + organizationUser.Id, + defaultCollectionName); } [Theory] @@ -252,9 +247,10 @@ public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectio await sutProvider.GetDependency() .DidNotReceive() - .CreateAsync(Arg.Any(), - Arg.Any>(), - Arg.Any>()); + .UpsertDefaultCollectionAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()); } [Theory] @@ -290,9 +286,10 @@ public async Task AutomaticallyConfirmOrganizationUserAsync_WhenCreateDefaultCol var collectionException = new Exception("Collection creation failed"); sutProvider.GetDependency() - .CreateAsync(Arg.Any(), - Arg.Any>(), - Arg.Any>()) + .UpsertDefaultCollectionAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) .ThrowsAsync(collectionException); // Act diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index 86b068b88f35..bc2aaee32ac1 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -10,7 +10,6 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Platform.Push; using Bit.Core.Repositories; @@ -491,15 +490,10 @@ public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganiza await sutProvider.GetDependency() .Received(1) - .CreateAsync( - Arg.Is(c => - c.Name == collectionName && - c.OrganizationId == organization.Id && - c.Type == CollectionType.DefaultUserCollection), - Arg.Any>(), - Arg.Is>(cu => - cu.Single().Id == orgUser.Id && - cu.Single().Manage)); + .UpsertDefaultCollectionAsync( + organization.Id, + orgUser.Id, + collectionName); } [Theory, BitAutoData] diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/UpsertDefaultCollectionTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/UpsertDefaultCollectionTests.cs new file mode 100644 index 000000000000..17dc36d153a0 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/UpsertDefaultCollectionTests.cs @@ -0,0 +1,110 @@ +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; + +public class UpsertDefaultCollectionTests +{ + [Theory, DatabaseData] + public async Task UpsertDefaultCollectionAsync_ShouldCreateCollection_WhenUserDoesNotHaveDefaultCollection( + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + var defaultCollectionName = $"My Items - {organization.Id}"; + + // Act + var wasCreated = await collectionRepository.UpsertDefaultCollectionAsync( + organization.Id, + orgUser.Id, + defaultCollectionName); + + // Assert + Assert.True(wasCreated); + + var collectionDetails = await collectionRepository.GetManyByUserIdAsync(user.Id); + var defaultCollection = collectionDetails.SingleOrDefault(c => + c.OrganizationId == organization.Id && + c.Type == CollectionType.DefaultUserCollection); + + Assert.NotNull(defaultCollection); + Assert.Equal(defaultCollectionName, defaultCollection.Name); + Assert.True(defaultCollection.Manage); + Assert.False(defaultCollection.ReadOnly); + Assert.False(defaultCollection.HidePasswords); + } + + [Theory, DatabaseData] + public async Task UpsertDefaultCollectionAsync_ShouldReturnFalse_WhenUserAlreadyHasDefaultCollection( + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + var defaultCollectionName = $"My Items - {organization.Id}"; + + // Create initial collection + var firstWasCreated = await collectionRepository.UpsertDefaultCollectionAsync( + organization.Id, + orgUser.Id, + defaultCollectionName); + + // Act - Call again with same parameters + var secondWasCreated = await collectionRepository.UpsertDefaultCollectionAsync( + organization.Id, + orgUser.Id, + defaultCollectionName); + + // Assert + Assert.True(firstWasCreated); + Assert.False(secondWasCreated); + + // Verify only one default collection exists + var collectionDetails = await collectionRepository.GetManyByUserIdAsync(user.Id); + var defaultCollections = collectionDetails.Where(c => + c.OrganizationId == organization.Id && + c.Type == CollectionType.DefaultUserCollection).ToList(); + + Assert.Single(defaultCollections); + } + + [Theory, DatabaseData] + public async Task UpsertDefaultCollectionAsync_ShouldBeIdempotent_WhenCalledMultipleTimes( + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + var defaultCollectionName = $"My Items - {organization.Id}"; + + // Act - Call method 5 times + var tasks = Enumerable.Range(1, 5).Select(i => collectionRepository.UpsertDefaultCollectionAsync( + organization.Id, + orgUser.Id, + defaultCollectionName)); + var results = await Task.WhenAll(tasks); + + // Assert + Assert.Single(results, r => r); // First call should create successfully; all other results are implicitly false + + // Verify only one collection exists + var collectionDetails = await collectionRepository.GetManyByUserIdAsync(user.Id); + Assert.Single(collectionDetails, c => + c.OrganizationId == organization.Id && + c.Type == CollectionType.DefaultUserCollection); + } +} diff --git a/util/Migrator/DbScripts/2025-12-02_00_Collection_UpsertDefaultCollection.sql b/util/Migrator/DbScripts/2025-12-02_00_Collection_UpsertDefaultCollection.sql new file mode 100644 index 000000000000..160d4e3cc7c5 --- /dev/null +++ b/util/Migrator/DbScripts/2025-12-02_00_Collection_UpsertDefaultCollection.sql @@ -0,0 +1,97 @@ +-- Create the idempotent stored procedure for creating default collections +-- This procedure prevents duplicate "My Items" collections for users by checking +-- if a default collection already exists before attempting to create one. + +CREATE OR ALTER PROCEDURE [dbo].[Collection_UpsertDefaultCollection] + @CollectionId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @OrganizationUserId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @WasCreated BIT OUTPUT +AS +BEGIN + SET NOCOUNT ON + + -- Use SERIALIZABLE isolation level to prevent race conditions during concurrent calls + SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; + BEGIN TRANSACTION; + + BEGIN TRY + DECLARE @ExistingCollectionId UNIQUEIDENTIFIER; + + -- Check if this organization user already has a default collection + -- SERIALIZABLE ensures range locks prevent concurrent insertions + SELECT @ExistingCollectionId = c.Id + FROM [dbo].[Collection] c + INNER JOIN [dbo].[CollectionUser] cu ON cu.CollectionId = c.Id + WHERE cu.OrganizationUserId = @OrganizationUserId + AND c.OrganizationId = @OrganizationId + AND c.Type = 1; -- CollectionType.DefaultUserCollection + + -- If collection already exists, return early + IF @ExistingCollectionId IS NOT NULL + BEGIN + SET @WasCreated = 0; + COMMIT TRANSACTION; + RETURN; + END + + -- Create new default collection + SET @WasCreated = 1; + + -- Insert Collection + INSERT INTO [dbo].[Collection] + ( + [Id], + [OrganizationId], + [Name], + [ExternalId], + [CreationDate], + [RevisionDate], + [DefaultUserCollectionEmail], + [Type] + ) + VALUES + ( + @CollectionId, + @OrganizationId, + @Name, + NULL, -- ExternalId + @CreationDate, + @RevisionDate, + NULL, -- DefaultUserCollectionEmail + 1 -- CollectionType.DefaultUserCollection + ); + + -- Insert CollectionUser + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + VALUES + ( + @CollectionId, + @OrganizationUserId, + 0, -- ReadOnly = false + 0, -- HidePasswords = false + 1 -- Manage = true + ); + + -- Bump user account revision dates + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @CollectionId, @OrganizationId; + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH +END +GO