1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-25 06:42:22 -05:00
bitwarden/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs
Alex Morask 54e7fac4d9
[PM-18770] Convert Organization to Business Unit (#5610)
* [NO LOGIC] Rename MultiOrganizationEnterprise to BusinessUnit

* [Core] Add IMailService.SendBusinessUnitConversionInviteAsync

* [Core] Add BusinessUnitConverter

* [Admin] Add new permission

* [Admin] Add BusinessUnitConverterController

* [Admin] Add Convert to Business Unit button to Organization edit page

* [Api] Add OrganizationBillingController.SetupBusinessUnitAsync action

* [Multi] Propagate provider type to sync response

* [Multi] Put updates behind feature flag

* [Tests] BusinessUnitConverterTests

* Run dotnet format

* Fixing post-main merge compilation failure
2025-04-10 10:06:16 -04:00

186 lines
5.9 KiB
C#

#nullable enable
using Bit.Admin.Billing.Models;
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Admin.Billing.Controllers;
[Authorize]
[Route("organizations/billing/{organizationId:guid}/business-unit")]
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
public class BusinessUnitConversionController(
IBusinessUnitConverter businessUnitConverter,
IOrganizationRepository organizationRepository,
IProviderRepository providerRepository,
IProviderUserRepository providerUserRepository) : Controller
{
[HttpGet]
[RequirePermission(Permission.Org_Billing_ConvertToBusinessUnit)]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IActionResult> IndexAsync([FromRoute] Guid organizationId)
{
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
var model = new BusinessUnitConversionModel { Organization = organization };
var invitedProviderAdmin = await GetInvitedProviderAdminAsync(organization);
if (invitedProviderAdmin != null)
{
model.ProviderAdminEmail = invitedProviderAdmin.Email;
model.ProviderId = invitedProviderAdmin.ProviderId;
}
var success = ReadSuccessMessage();
if (!string.IsNullOrEmpty(success))
{
model.Success = success;
}
var errors = ReadErrorMessages();
if (errors is { Count: > 0 })
{
model.Errors = errors;
}
return View(model);
}
[HttpPost]
[RequirePermission(Permission.Org_Billing_ConvertToBusinessUnit)]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IActionResult> InitiateAsync(
[FromRoute] Guid organizationId,
BusinessUnitConversionModel model)
{
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
var result = await businessUnitConverter.InitiateConversion(
organization,
model.ProviderAdminEmail!);
return result.Match(
providerId => RedirectToAction("Edit", "Providers", new { id = providerId }),
errors =>
{
PersistErrorMessages(errors);
return RedirectToAction("Index", new { organizationId });
});
}
[HttpPost("reset")]
[RequirePermission(Permission.Org_Billing_ConvertToBusinessUnit)]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IActionResult> ResetAsync(
[FromRoute] Guid organizationId,
BusinessUnitConversionModel model)
{
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
await businessUnitConverter.ResetConversion(organization, model.ProviderAdminEmail!);
PersistSuccessMessage("Business unit conversion was successfully reset.");
return RedirectToAction("Index", new { organizationId });
}
[HttpPost("resend-invite")]
[RequirePermission(Permission.Org_Billing_ConvertToBusinessUnit)]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IActionResult> ResendInviteAsync(
[FromRoute] Guid organizationId,
BusinessUnitConversionModel model)
{
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
await businessUnitConverter.ResendConversionInvite(organization, model.ProviderAdminEmail!);
PersistSuccessMessage($"Invite was successfully resent to {model.ProviderAdminEmail}.");
return RedirectToAction("Index", new { organizationId });
}
private async Task<ProviderUser?> GetInvitedProviderAdminAsync(
Organization organization)
{
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
if (provider is not
{
Type: ProviderType.BusinessUnit,
Status: ProviderStatusType.Pending
})
{
return null;
}
var providerUsers =
await providerUserRepository.GetManyByProviderAsync(provider.Id, ProviderUserType.ProviderAdmin);
if (providerUsers.Count != 1)
{
return null;
}
var providerUser = providerUsers.First();
return providerUser is
{
Type: ProviderUserType.ProviderAdmin,
Status: ProviderUserStatusType.Invited,
UserId: not null
} ? providerUser : null;
}
private const string _errors = "errors";
private const string _success = "Success";
private void PersistSuccessMessage(string message) => TempData[_success] = message;
private void PersistErrorMessages(List<string> errors)
{
var input = string.Join("|", errors);
TempData[_errors] = input;
}
private string? ReadSuccessMessage() => ReadTempData<string>(_success);
private List<string>? ReadErrorMessages()
{
var output = ReadTempData<string>(_errors);
return string.IsNullOrEmpty(output) ? null : output.Split('|').ToList();
}
private T? ReadTempData<T>(string key) => TempData.TryGetValue(key, out var obj) && obj is T value ? value : default;
}