mirror of
https://github.com/bitwarden/server.git
synced 2025-07-17 15:40:59 -05:00
[PM-15160] Create ResellerClientOrganizationSignUpCommand (#5981)
* Implement ResellerClientOrganizationSignUpCommand for signing up reseller client organizations with email invitations and error handling * Refactor ProvidersController to replace IOrganizationService with IResellerClientOrganizationSignUpCommand for organization sign-up process * Remove CreatePendingOrganization method from IOrganizationService and its implementation in OrganizationService * Add IResellerClientOrganizationSignUpCommand to service collection for organization sign-up * Add comment to clarify organization deletion process in ResellerClientOrganizationSignUpCommand
This commit is contained in:
@ -0,0 +1,130 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
|
||||
public record ResellerClientOrganizationSignUpResponse(
|
||||
Organization Organization,
|
||||
OrganizationUser OwnerOrganizationUser);
|
||||
|
||||
/// <summary>
|
||||
/// Command for signing up reseller client organizations in a pending state.
|
||||
/// </summary>
|
||||
public interface IResellerClientOrganizationSignUpCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Sign up a reseller client organization. The organization will be created in a pending state
|
||||
/// (disabled and with Pending status) and the owner will be invited via email. The organization
|
||||
/// will become active once the owner accepts the invitation.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization to create.</param>
|
||||
/// <param name="ownerEmail">The email of the organization owner who will be invited.</param>
|
||||
/// <returns>A response containing the created pending organization and invited owner user.</returns>
|
||||
Task<ResellerClientOrganizationSignUpResponse> SignUpResellerClientAsync(
|
||||
Organization organization,
|
||||
string ownerEmail);
|
||||
}
|
||||
|
||||
public class ResellerClientOrganizationSignUpCommand : IResellerClientOrganizationSignUpCommand
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
|
||||
private readonly IPaymentService _paymentService;
|
||||
|
||||
public ResellerClientOrganizationSignUpCommand(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService,
|
||||
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
|
||||
IPaymentService paymentService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_eventService = eventService;
|
||||
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
|
||||
_paymentService = paymentService;
|
||||
}
|
||||
|
||||
public async Task<ResellerClientOrganizationSignUpResponse> SignUpResellerClientAsync(
|
||||
Organization organization,
|
||||
string ownerEmail)
|
||||
{
|
||||
try
|
||||
{
|
||||
var createdOrganization = await CreateOrganizationAsync(organization);
|
||||
var ownerOrganizationUser = await CreateAndInviteOwnerAsync(createdOrganization, ownerEmail);
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);
|
||||
|
||||
return new ResellerClientOrganizationSignUpResponse(organization, ownerOrganizationUser);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _paymentService.CancelAndRecoverChargesAsync(organization);
|
||||
|
||||
if (organization.Id != default)
|
||||
{
|
||||
// Deletes the organization and all related data, including its owner user
|
||||
await _organizationRepository.DeleteAsync(organization);
|
||||
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Organization> CreateOrganizationAsync(Organization organization)
|
||||
{
|
||||
organization.Id = CoreHelpers.GenerateComb();
|
||||
organization.Enabled = false;
|
||||
organization.Status = OrganizationStatusType.Pending;
|
||||
|
||||
await _organizationRepository.CreateAsync(organization);
|
||||
await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
ApiKey = CoreHelpers.SecureRandomString(30),
|
||||
Type = OrganizationApiKeyType.Default,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
});
|
||||
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
return organization;
|
||||
}
|
||||
|
||||
private async Task<OrganizationUser> CreateAndInviteOwnerAsync(Organization organization, string ownerEmail)
|
||||
{
|
||||
var ownerOrganizationUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = null,
|
||||
Email = ownerEmail,
|
||||
Key = null,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
};
|
||||
|
||||
await _organizationUserRepository.CreateAsync(ownerOrganizationUser);
|
||||
|
||||
await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(
|
||||
users: [ownerOrganizationUser],
|
||||
organization: organization,
|
||||
initOrganization: true));
|
||||
|
||||
return ownerOrganizationUser;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Entities;
|
||||
@ -42,7 +41,6 @@ public interface IOrganizationService
|
||||
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
|
||||
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
|
||||
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
||||
Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd);
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -1705,27 +1704,4 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted)
|
||||
{
|
||||
organization.Id = CoreHelpers.GenerateComb();
|
||||
organization.Enabled = false;
|
||||
organization.Status = OrganizationStatusType.Pending;
|
||||
|
||||
await SignUpAsync(organization, default, null, null, true);
|
||||
|
||||
var ownerOrganizationUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = null,
|
||||
Email = ownerEmail,
|
||||
Key = null,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
};
|
||||
await _organizationUserRepository.CreateAsync(ownerOrganizationUser);
|
||||
|
||||
await SendInviteAsync(ownerOrganizationUser, organization, true);
|
||||
await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();
|
||||
services.AddScoped<IProviderClientOrganizationSignUpCommand, ProviderClientOrganizationSignUpCommand>();
|
||||
services.AddScoped<IResellerClientOrganizationSignUpCommand, ResellerClientOrganizationSignUpCommand>();
|
||||
}
|
||||
|
||||
private static void AddOrganizationDeleteCommands(this IServiceCollection services)
|
||||
|
Reference in New Issue
Block a user