mirror of
https://github.com/bitwarden/server.git
synced 2025-07-03 00:52:49 -05:00
[EC-459 / EC-428] Admin panel: Add Provider Type to list and creation flow (#2593)
* [EC-427] Add columns 'Type' and 'BillingPhone' to Provider table * [EC-427] Provider table Type and BillingPhone MySql migrations * [EC-427] Provider table Type and BillingPhone Postgres migrations * [EC-427] Add mysql migration script * [EC-427] Add mysql migration script * [EC-427] Updated Provider sql script to include default column value * [EC-427] Removed default value from Provider.Type column * [EC-427] Changed migration script to include a default value constraint instead of updating the null type * [EC-427] Updated Sql project Provider table script * [EC-427] Changed migration script to use 'Create OR Alter' for views and sprocs * [EC-427] Added default values for 'BillingPhone' and 'Type' fields on sprocs [dbo].[Provider_Create] and [dbo].[Provider_Update] * [EC-427] Adjusting metadata in migration script * [EC-427] Updated Provider sprocs SQL script files * [EC-427] Fixed migration script * [EC-427] Added sqlite migration * [EC-427] Add missing Provider_Update sproc default value * [EC-427] Added missing GO action to migration script * [EC-459] Added Type column to Providers list * [EC-428] Added Type, BusinessName and BillingEmail to CreateProviderModel * [EC-428] Updated Create Provider view to include new fields * [EC-428] Updated ProviderService to not create a ProviderUser for the type Reseller * [EC-428] Added custom validation for Provider fields depending on selected Type * [EC-428] Redirect to Edit after creating Provider * [EC-428] Setting Provider status as Created for Resellers * [EC-428] Redirect on Provider creation depending if self host server * [EC-428] Split ProviderService.CreateAsync into two methods: CreateMspAsync and CreateResellerAsync * [EC-428] Created ICreateProviderCommand and added service for injection on Admin.Startup * [EC-428] Modified Provider views to use DisplayName attribute values * [EC-428] Moved ICreateProviderCommand to Core project * [EC-428] Adding ICreateProviderCommand injection next to IProviderService * [EC-428] Moved CreateProviderCommand to Commercial.Core project * [EC-459] Added Type column to Providers list * [EC-428] Added Type, BusinessName and BillingEmail to CreateProviderModel * [EC-428] Updated Create Provider view to include new fields * [EC-428] Updated ProviderService to not create a ProviderUser for the type Reseller * [EC-428] Added custom validation for Provider fields depending on selected Type * [EC-428] Redirect to Edit after creating Provider * [EC-428] Setting Provider status as Created for Resellers * [EC-428] Redirect on Provider creation depending if self host server * [EC-428] Split ProviderService.CreateAsync into two methods: CreateMspAsync and CreateResellerAsync * [EC-428] Created ICreateProviderCommand and added service for injection on Admin.Startup * [EC-428] Modified Provider views to use DisplayName attribute values * [EC-428] Moved ICreateProviderCommand to Core project * [EC-428] Adding ICreateProviderCommand injection next to IProviderService * [EC-428] Moved CreateProviderCommand to Commercial.Core project * [EC-428] Moved CreateProviderCommand to namespace Bit.Commercial.Core.Providers
This commit is contained in:
@ -0,0 +1,62 @@
|
|||||||
|
using Bit.Core.Entities.Provider;
|
||||||
|
using Bit.Core.Enums.Provider;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Providers.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Providers;
|
||||||
|
|
||||||
|
public class CreateProviderCommand : ICreateProviderCommand
|
||||||
|
{
|
||||||
|
private readonly IProviderRepository _providerRepository;
|
||||||
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
|
private readonly IProviderService _providerService;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
|
||||||
|
public CreateProviderCommand(
|
||||||
|
IProviderRepository providerRepository,
|
||||||
|
IProviderUserRepository providerUserRepository,
|
||||||
|
IProviderService providerService,
|
||||||
|
IUserRepository userRepository)
|
||||||
|
{
|
||||||
|
_providerRepository = providerRepository;
|
||||||
|
_providerUserRepository = providerUserRepository;
|
||||||
|
_providerService = providerService;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateMspAsync(Provider provider, string ownerEmail)
|
||||||
|
{
|
||||||
|
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||||
|
if (owner == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
|
||||||
|
|
||||||
|
var providerUser = new ProviderUser
|
||||||
|
{
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
UserId = owner.Id,
|
||||||
|
Type = ProviderUserType.ProviderAdmin,
|
||||||
|
Status = ProviderUserStatusType.Confirmed,
|
||||||
|
};
|
||||||
|
await _providerUserRepository.CreateAsync(providerUser);
|
||||||
|
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateResellerAsync(Provider provider)
|
||||||
|
{
|
||||||
|
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)
|
||||||
|
{
|
||||||
|
provider.Status = status;
|
||||||
|
provider.Enabled = true;
|
||||||
|
provider.UseEvents = true;
|
||||||
|
await _providerRepository.CreateAsync(provider);
|
||||||
|
}
|
||||||
|
}
|
@ -53,33 +53,6 @@ public class ProviderService : IProviderService
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAsync(string ownerEmail)
|
|
||||||
{
|
|
||||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
|
||||||
if (owner == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider = new Provider
|
|
||||||
{
|
|
||||||
Status = ProviderStatusType.Pending,
|
|
||||||
Enabled = true,
|
|
||||||
UseEvents = true,
|
|
||||||
};
|
|
||||||
await _providerRepository.CreateAsync(provider);
|
|
||||||
|
|
||||||
var providerUser = new ProviderUser
|
|
||||||
{
|
|
||||||
ProviderId = provider.Id,
|
|
||||||
UserId = owner.Id,
|
|
||||||
Type = ProviderUserType.ProviderAdmin,
|
|
||||||
Status = ProviderUserStatusType.Confirmed,
|
|
||||||
};
|
|
||||||
await _providerUserRepository.CreateAsync(providerUser);
|
|
||||||
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
||||||
{
|
{
|
||||||
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
||||||
@ -456,7 +429,7 @@ public class ProviderService : IProviderService
|
|||||||
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail)
|
public async Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail)
|
||||||
{
|
{
|
||||||
var token = _dataProtector.Protect($"ProviderSetupInvite {provider.Id} {ownerEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
var token = _dataProtector.Protect($"ProviderSetupInvite {provider.Id} {ownerEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||||
await _mailService.SendProviderSetupInviteEmailAsync(provider, token, ownerEmail);
|
await _mailService.SendProviderSetupInviteEmailAsync(provider, token, ownerEmail);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Commercial.Core.Services;
|
using Bit.Commercial.Core.Providers;
|
||||||
|
using Bit.Commercial.Core.Services;
|
||||||
|
using Bit.Core.Providers.Interfaces;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@ -9,5 +11,6 @@ public static class ServiceCollectionExtensions
|
|||||||
public static void AddCommercialCoreServices(this IServiceCollection services)
|
public static void AddCommercialCoreServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IProviderService, ProviderService>();
|
services.AddScoped<IProviderService, ProviderService>();
|
||||||
|
services.AddScoped<ICreateProviderCommand, CreateProviderCommand>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
using Bit.Commercial.Core.Providers;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Entities.Provider;
|
||||||
|
using Bit.Core.Enums.Provider;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Test.ProviderFeatures;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class CreateProviderCommandTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.CreateMspAsync(provider, default));
|
||||||
|
Assert.Contains("Invalid owner.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||||
|
|
||||||
|
await sutProvider.Sut.CreateMspAsync(provider, user.Email);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
|
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Reseller;
|
||||||
|
|
||||||
|
await sutProvider.Sut.CreateResellerAsync(provider);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
|
await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ using Microsoft.AspNetCore.DataProtection;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReturnsExtensions;
|
using NSubstitute.ReturnsExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using Provider = Bit.Core.Entities.Provider.Provider;
|
||||||
using ProviderUser = Bit.Core.Entities.Provider.ProviderUser;
|
using ProviderUser = Bit.Core.Entities.Provider.ProviderUser;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Services;
|
namespace Bit.Commercial.Core.Test.Services;
|
||||||
@ -24,26 +25,6 @@ namespace Bit.Commercial.Core.Test.Services;
|
|||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class ProviderServiceTests
|
public class ProviderServiceTests
|
||||||
{
|
{
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task CreateAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
|
||||||
{
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.CreateAsync(default));
|
|
||||||
Assert.Contains("Invalid owner.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task CreateAsync_Success(User user, SutProvider<ProviderService> sutProvider)
|
|
||||||
{
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
|
||||||
|
|
||||||
await sutProvider.Sut.CreateAsync(user.Email);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
|
||||||
await sutProvider.GetDependency<IMailService>().ReceivedWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default, default);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
@ -229,6 +210,14 @@ public class ProviderServiceTests
|
|||||||
Assert.True(result.All(r => r.Item2 == ""));
|
Assert.True(result.All(r => r.Item2 == ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SendProviderSetupInviteEmailAsync_Success(Provider provider, string email, SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
await sutProvider.Sut.SendProviderSetupInviteEmailAsync(provider, email);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderSetupInviteEmailAsync(provider, Arg.Any<string>(), email);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task AcceptUserAsync_UserIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
public async Task AcceptUserAsync_UserIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using Bit.Admin.Models;
|
using Bit.Admin.Models;
|
||||||
using Bit.Core.Entities.Provider;
|
using Bit.Core.Entities.Provider;
|
||||||
|
using Bit.Core.Enums.Provider;
|
||||||
|
using Bit.Core.Providers.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@ -19,10 +21,16 @@ public class ProvidersController : Controller
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
|
private readonly ICreateProviderCommand _createProviderCommand;
|
||||||
|
|
||||||
public ProvidersController(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
public ProvidersController(
|
||||||
IProviderOrganizationRepository providerOrganizationRepository, IProviderService providerService,
|
IProviderRepository providerRepository,
|
||||||
GlobalSettings globalSettings, IApplicationCacheService applicationCacheService)
|
IProviderUserRepository providerUserRepository,
|
||||||
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
|
IProviderService providerService,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
IApplicationCacheService applicationCacheService,
|
||||||
|
ICreateProviderCommand createProviderCommand)
|
||||||
{
|
{
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
@ -30,6 +38,7 @@ public class ProvidersController : Controller
|
|||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
|
_createProviderCommand = createProviderCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index(string name = null, string userEmail = null, int page = 1, int count = 25)
|
public async Task<IActionResult> Index(string name = null, string userEmail = null, int page = 1, int count = 25)
|
||||||
@ -75,9 +84,18 @@ public class ProvidersController : Controller
|
|||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _providerService.CreateAsync(model.OwnerEmail);
|
var provider = model.ToProvider();
|
||||||
|
switch (provider.Type)
|
||||||
|
{
|
||||||
|
case ProviderType.Msp:
|
||||||
|
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail);
|
||||||
|
break;
|
||||||
|
case ProviderType.Reseller:
|
||||||
|
await _createProviderCommand.CreateResellerAsync(provider);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Edit", new { id = provider.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> View(Guid id)
|
public async Task<IActionResult> View(Guid id)
|
||||||
|
@ -1,12 +1,59 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Entities.Provider;
|
||||||
|
using Bit.Core.Enums.Provider;
|
||||||
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
namespace Bit.Admin.Models;
|
namespace Bit.Admin.Models;
|
||||||
|
|
||||||
public class CreateProviderModel
|
public class CreateProviderModel : IValidatableObject
|
||||||
{
|
{
|
||||||
public CreateProviderModel() { }
|
public CreateProviderModel() { }
|
||||||
|
|
||||||
|
[Display(Name = "Provider Type")]
|
||||||
|
public ProviderType Type { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Owner Email")]
|
[Display(Name = "Owner Email")]
|
||||||
[Required]
|
|
||||||
public string OwnerEmail { get; set; }
|
public string OwnerEmail { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Business Name")]
|
||||||
|
public string BusinessName { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Primary Billing Email")]
|
||||||
|
public string BillingEmail { get; set; }
|
||||||
|
|
||||||
|
public virtual Provider ToProvider()
|
||||||
|
{
|
||||||
|
return new Provider()
|
||||||
|
{
|
||||||
|
Type = Type,
|
||||||
|
BusinessName = BusinessName,
|
||||||
|
BillingEmail = BillingEmail?.ToLowerInvariant().Trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
switch (Type)
|
||||||
|
{
|
||||||
|
case ProviderType.Msp:
|
||||||
|
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||||
|
{
|
||||||
|
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName();
|
||||||
|
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProviderType.Reseller:
|
||||||
|
if (string.IsNullOrWhiteSpace(BusinessName))
|
||||||
|
{
|
||||||
|
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName();
|
||||||
|
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(BillingEmail))
|
||||||
|
{
|
||||||
|
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName();
|
||||||
|
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,56 @@
|
|||||||
@model CreateProviderModel
|
@using Bit.SharedWeb.Utilities
|
||||||
|
@model CreateProviderModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Create Provider";
|
ViewData["Title"] = "Create Provider";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
function toggleProviderTypeInfo(value) {
|
||||||
|
document.querySelectorAll('[id^="info-"]').forEach(el => { el.classList.add('d-none'); });
|
||||||
|
document.getElementById('info-' + value).classList.remove('d-none');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
<h1>Create Provider</h1>
|
<h1>Create Provider</h1>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Type" class="h2"></label>
|
||||||
|
@foreach(ProviderType providerType in Enum.GetValues(typeof(ProviderType)))
|
||||||
|
{
|
||||||
|
var providerTypeValue = (int)providerType;
|
||||||
|
<div class="form-check">
|
||||||
|
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input", onclick=$"toggleProviderTypeInfo({providerTypeValue})" })
|
||||||
|
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
|
||||||
|
<br/>
|
||||||
|
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted ml-3 align-top", @for = $"providerType-{providerTypeValue}" })
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="@($"info-{(int)ProviderType.Msp}")" class="form-group @(Model.Type != ProviderType.Msp ? "d-none" : string.Empty)">
|
||||||
|
<h2>MSP Info</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="OwnerEmail"></label>
|
<label asp-for="OwnerEmail"></label>
|
||||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
||||||
|
<h2>Reseller Info</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="BusinessName"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="BusinessName">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="BillingEmail"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="BillingEmail">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@model ProvidersModel
|
@using Bit.SharedWeb.Utilities
|
||||||
|
@model ProvidersModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Providers";
|
ViewData["Title"] = "Providers";
|
||||||
}
|
}
|
||||||
@ -25,6 +26,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th style="width: 190px;">Provider Type</th>
|
||||||
<th style="width: 190px;">Status</th>
|
<th style="width: 190px;">Status</th>
|
||||||
<th style="width: 150px;">Created</th>
|
<th style="width: 150px;">Created</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -44,6 +46,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
|
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
|
||||||
<td>@provider.Status</td>
|
<td>@provider.Status</td>
|
||||||
<td>
|
<td>
|
||||||
<span title="@provider.CreationDate.ToString()">
|
<span title="@provider.CreationDate.ToString()">
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
namespace Bit.Core.Enums.Provider;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Core.Enums.Provider;
|
||||||
|
|
||||||
public enum ProviderType : byte
|
public enum ProviderType : byte
|
||||||
{
|
{
|
||||||
|
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization")]
|
||||||
Msp = 0,
|
Msp = 0,
|
||||||
|
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing")]
|
||||||
Reseller = 1,
|
Reseller = 1,
|
||||||
}
|
}
|
||||||
|
9
src/Core/Providers/Interfaces/ICreateProviderCommand.cs
Normal file
9
src/Core/Providers/Interfaces/ICreateProviderCommand.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Entities.Provider;
|
||||||
|
|
||||||
|
namespace Bit.Core.Providers.Interfaces;
|
||||||
|
|
||||||
|
public interface ICreateProviderCommand
|
||||||
|
{
|
||||||
|
Task CreateMspAsync(Provider provider, string ownerEmail);
|
||||||
|
Task CreateResellerAsync(Provider provider);
|
||||||
|
}
|
@ -7,7 +7,6 @@ namespace Bit.Core.Services;
|
|||||||
|
|
||||||
public interface IProviderService
|
public interface IProviderService
|
||||||
{
|
{
|
||||||
Task CreateAsync(string ownerEmail);
|
|
||||||
Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key);
|
Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key);
|
||||||
Task UpdateAsync(Provider provider, bool updateBilling = false);
|
Task UpdateAsync(Provider provider, bool updateBilling = false);
|
||||||
|
|
||||||
@ -26,5 +25,6 @@ public interface IProviderService
|
|||||||
Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId);
|
Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId);
|
||||||
Task LogProviderAccessToOrganizationAsync(Guid organizationId);
|
Task LogProviderAccessToOrganizationAsync(Guid organizationId);
|
||||||
Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId);
|
Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId);
|
||||||
|
Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +7,6 @@ namespace Bit.Core.Services;
|
|||||||
|
|
||||||
public class NoopProviderService : IProviderService
|
public class NoopProviderService : IProviderService
|
||||||
{
|
{
|
||||||
public Task CreateAsync(string ownerEmail) => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key) => throw new NotImplementedException();
|
public Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key) => throw new NotImplementedException();
|
||||||
|
|
||||||
public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException();
|
public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException();
|
||||||
@ -34,4 +32,5 @@ public class NoopProviderService : IProviderService
|
|||||||
public Task LogProviderAccessToOrganizationAsync(Guid organizationId) => throw new NotImplementedException();
|
public Task LogProviderAccessToOrganizationAsync(Guid organizationId) => throw new NotImplementedException();
|
||||||
|
|
||||||
public Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid userId) => throw new NotImplementedException();
|
public Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid userId) => throw new NotImplementedException();
|
||||||
|
public Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail) => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
21
src/SharedWeb/Utilities/DisplayAttributeHelpers.cs
Normal file
21
src/SharedWeb/Utilities/DisplayAttributeHelpers.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
|
public static class DisplayAttributeHelpers
|
||||||
|
{
|
||||||
|
public static DisplayAttribute GetDisplayAttribute(this Enum enumValue)
|
||||||
|
{
|
||||||
|
return enumValue.GetType()
|
||||||
|
.GetMember(enumValue.ToString())
|
||||||
|
.First()
|
||||||
|
.GetCustomAttribute<DisplayAttribute>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DisplayAttribute GetDisplayAttribute<T>(this string property)
|
||||||
|
{
|
||||||
|
MemberInfo propertyInfo = typeof(T).GetProperty(property);
|
||||||
|
return propertyInfo?.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user