1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-05 10:02:47 -05:00

[AC-1081] Merge feature/billing-obfuscation (#2665)

* [EC-1014] Create Organization Status (Pending/Created) (#2610)

* [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-428] Redirect to Edit after creating Provider

* Revert "[EC-428] Redirect to Edit after creating Provider"

This reverts commit 6347bca1ed.

* [EC-1014] Create OrganizationStatusType and add Status column to Organizations table

* [EC-1014] Added EF migrations

* [EC-1014] dotnet format

* [EC-1014] Changed Organization.Status from SMALLINT to TINYINT

* [EC-1014] Set Organization.Status default value = 1

* [EC-1014] Setting Organization.Status default value as 1

* [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

* [EC-429] Provider details screen updated with Type, BillingPhone and Organization details (#2666)

* [EC-430] Admin portal: Update organization information screen (#2672)

* [EC-430] Added ProviderOrganizationProviderDetailsView to get Provider details for an Organization

* [EC-430] Added Provider information to Organization Edit/View on Admin panel

* [EC-430] Remove "Add to Reseller" button

* [EC-430] Removed unused property OrganizationEditModel.ClientOwnerEmail

* [EC-430] Replaced IProviderOrganizationRepository.GetProviderDetailsByOrganizationAsync with IProviderRepository.GetByOrganizationIdAsync

* [EC-430] Deleted ProviderOrganizationProviderDetails and ProviderOrganizationProviderDetailsReadByOrganizationIdQuery

* [EC-429] Only show Create/Add Existing Organization buttons for Reseller providers (#2723)

* [EC-432] Add existing Organizations to Provider (#2683)

* [EC-432] Added ProviderOrganizationUnassignedOrganizationDetails_Search stored procedure

* [EC-432] Added IProviderOrganizationRepository.SearchAsync

* [EC-432] Created controller ProviderOrganizationsController to assign Organizations to a Provider

* [EC-432] Filter existing organizations by plans Enterprise or Team

* [EC-432] Existing Organization name links to edit page

* [EC-432] EF filtering out existing organizations by plan type enterprise or teams

* [EC-432] Creating multiple ProviderOrganization records

* [EC-432] Added ProviderOrganizationUnassignedOrganizationDetails_Search stored procedure

* [EC-432] Added IProviderOrganizationRepository.SearchAsync

* [EC-432] Created controller ProviderOrganizationsController to assign Organizations to a Provider

* [EC-432] Filter existing organizations by plans Enterprise or Team

* [EC-432] Existing Organization name links to edit page

* [EC-432] EF filtering out existing organizations by plan type enterprise or teams

* [EC-432] Creating multiple ProviderOrganization records

* [EC-432] Renamed migration script and added missing sproc

* [EC-432] Saving multiple events for the created ProviderOrganizations

* [EC-432] Included unit testing for ProviderService.AddOrganizations and EventService.LogProviderOrganizationEventsAsync

* [EC-432] Removed async from NoopEventService.LogProviderOrganizationEventsAsync

* [EC-432] Remove unused dependency setup in ProviderServiceTests.AddOrganizations_Success

* [EC-432] Renamed AddOrganizations to AddOrganizationsToReseller and removed addingUserId and key arguments

* [EC-432] Added DisplayName attributes to ProviderOrganizationViewModel and used them in the view

* [EC-432] Reverted changes to input fields

* [EC-432] Moved unassigned organizations search to Organizations repo

* [EC-432] Moved AddExistingOrganization action to ProvidersController

* [EC-432] dotnet format

* [EC-432] Fixed unit test issues

* [EC-432] Removed unnecessary Html.DisplayNameFor for labels

* [EC-432] Renamed OrganizationSearchViewModel to OrganizationUnassignedToProviderSearchViewModel

* [EC-432] Modified IEventService.LogProviderOrganizationEventsAsync to receive an IEnumerable as parameter

* [EC-432] Updated IProviderOrganizationRepository and replaced CreateWithManyOrganizations method with CreateManyAsync

* [EC-432] Deleted ProviderOrganization_CreateWithManyOrganizations

* [AC-432] Simplified Organization_UnassignedToProviderSearch query

* [AC-432] Removed unnecessary setup

* [EC-432] Checking if stored procedure exists before creating

* [EC-432] Renamed migration file to recent date

* [EC-435] Admin Portal: Add new Organization creation flow UI (#2707)

* [EC-435] Created _OrganizationForm partial view. Added actions for creating an Organization assigned to a provider

* [EC-435] Remove logic for creating an organization

* [EC-435] Created partial view _OrganizationFormScripts

* [EC-435] Remove unused ReferenceEventType

* [EC-435] Added TODO comment on Organization Create

* [EC-435] Checking if Provider type is Reseller on creating new assigned organization

* [EC-435] Setting the Organization plan type as TeamsMonthly by default when adding to a provider

* [EC-435] Removing unused buttons

* [EC-435] Switched hidden fields to form submit route value

* [EC-435] Moved _OrganizationForm and _OrganizationFormScripts to Shared folder

* [EC-435] Moved Create organization actions from OrganizationsController to ProvidersController

* [EC-435] Fixing bug on saving Organization that would have BillingEmail as null

* [EC-435] Added null check to Provider

* [EC-435] Moved trial buttons script logic to Edit view

* [AC-431] Add new organization invite process (#2737)

* [EC-435] Created _OrganizationForm partial view. Added actions for creating an Organization assigned to a provider

* [EC-435] Remove logic for creating an organization

* [EC-435] Created partial view _OrganizationFormScripts

* [EC-435] Remove unused ReferenceEventType

* [EC-435] Added TODO comment on Organization Create

* [EC-435] Checking if Provider type is Reseller on creating new assigned organization

* [EC-435] Setting the Organization plan type as TeamsMonthly by default when adding to a provider

* [EC-435] Removing unused buttons

* [EC-435] Switched hidden fields to form submit route value

* [EC-435] Moved _OrganizationForm and _OrganizationFormScripts to Shared folder

* [EC-435] Moved Create organization actions from OrganizationsController to ProvidersController

* [AC-431] Added new ReferenceEventType OrganizationCreatedByAdmin

* [AC-431] Added method IOrganizationService.CreateOrganization

* [AC-431] Creating new Organization with Pending status and assigning to Provider

* [AC-431] Added method to IMailService to send invitation to initialize org

* [AC-431] Added methods CreatePendingOrganization and InitPendingOrganization to IOrganizationService

* [AC-431] Org invite includes initOrganization parameter

* [AC-431] Modified existing Accept organization user action to initialize org

* [AC-431] Updated ProvidersController method name

* [AC-431] Created OrganizationUserInitInvitedViewModel to link to 'accept-init-organization' url

* [AC-431] Added action AcceptInit to OrganizationUsersController

* [AC-431] Resend owner invite

* [AC-431] dotnet format

* [AC-431] Removed unused parameter 'addingUserId' from IProviderService.AddOrganization

* [AC-431] Removed setting manual values for CreationDate and RevisionDate

* [AC-431] Updated OrganizationService.InitPendingOrganization to throw exceptions when the Organization does not meet the required criteria

* [AC-431] Modified OrganizationUserInitInvitedViewModel to inherit properties from OrganizationUserInvitedViewModel

* [AC-431] Removed unecessary parameter check

* [AC-431] Moved method description to IOrganizationService.InitPendingOrganization

* [AC-431] Moved ApplicationCacheService.UpsertOrganizationAbilityAsync and ReferenceEventService.RaiseEventAsync to OrganizationService

* [AC-431] Creating collection after creating organization

* [EC-435] Fixing bug on saving Organization that would have BillingEmail as null

* [AC-431] Deleted OrganizationUserInitInvitedViewModel and added parameter InitOrganization to OrganizationUserInvitedViewModel.cs

* [AC-431] Checking if the user has any existing SingleOrg policies before initializing an Org

* [AC-431] Remove commented code

* [EC-435] Added null check to Provider

* [EC-435] Moved trial buttons script logic to Edit view

* [AC-431] Added EncryptedString attribute to OrganizationUserAcceptInitRequestModel.CollectionName

* [AC-431] Refactored plan check condition

* [AC-431] Remove duplicate _applicationCacheService.UpsertOrganizationAbilityAsync call

* [AC-431] Removed IMailService.SendOrganizationInitInviteEmailAsync

* [AC-431] Added parameters ClaimsPrincipal and IUserService to IOrganizationService.CreatePendingOrganization

* [AC-434] Hide Billing screen for Reseller clients (#2783)

* [AC-434] Added ProviderType to ProfileOrganizationResponseModel

* [AC-434] Migration script

* [AC-434] Fixed indentation on migration script

* [AC-434] Hiding sensitive subscription data if the user does not have permissions

* [AC-434] Fixed missing dependency in unit test

* [AC-434] Altered BillingSubscription.Amount and BillingSubscriptionUpcomingInvoice.Amount to nullable

* [AC-434] Replaced CurrentContext.ManageBilling with ViewBillingHistory, ViewSubscription, EditSubscription and EditPaymentMethods

* [AC-434] Reverted change on BillingSubscription.Amount and now setting Subscription.Items = null when User does not have permission

* [AC-434] Added ProviderOrganizationProviderDetails_ReadByUserId

* [AC-434] Added IProviderOrganizationRepository.GetManyByUserAsync

* [AC-434] Added CurrentContext.GetOrganizationProviderDetails

* [AC-434] Remove unneeded join Organization table

* [AC-1255] Search Existing Organizations by partial Email (#2830)

* [AC-1255] Added email search field input validation

* [AC-1255] Reverted added email pattern

* [AC-1255] Modified Organization search by Email to search using substring

* [AC-1276] Displaying an Organizations pending owners if the Organization is in a Pending status (#2834)

* [AC-432] Checking that an existing Organization is not assigned to any Provider before being assigned (#2840)

* [AC-432] Checking if any of the selected Organizations is already assigned to a Provider

* [AC-432] Changed ProviderOrganization_ReadByOrganizationIds to only get count

* [AC-432] Replaced IProviderOrganizationRepository.GetCountByOrganizationIdsAsync with call to IProviderOrganizationRepository.GetByOrganizationId

* [AC-432] undo new line

* [AC-432] Fixed unit test

* Revert "[AC-432] Replaced IProviderOrganizationRepository.GetCountByOrganizationIdsAsync with call to IProviderOrganizationRepository.GetByOrganizationId"

This reverts commit ee6e095e88.

# Conflicts:
#	util/Migrator/DbScripts/2023-03-22_00_ProviderAddExistingOrganizations.sql

* [AC-432] Created new migration script for ProviderOrganization_ReadCountByOrganizationIds
This commit is contained in:
Rui Tomé
2023-04-14 11:13:16 +01:00
committed by GitHub
parent 03c740dffb
commit f5a8cf5c9c
96 changed files with 8689 additions and 431 deletions

View File

@ -17,6 +17,7 @@ namespace Bit.Admin.Controllers;
[Authorize]
public class OrganizationsController : Controller
{
private readonly IOrganizationService _organizationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
@ -31,9 +32,11 @@ public class OrganizationsController : Controller
private readonly GlobalSettings _globalSettings;
private readonly IReferenceEventService _referenceEventService;
private readonly IUserService _userService;
private readonly IProviderRepository _providerRepository;
private readonly ILogger<OrganizationsController> _logger;
public OrganizationsController(
IOrganizationService organizationService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationConnectionRepository organizationConnectionRepository,
@ -48,8 +51,10 @@ public class OrganizationsController : Controller
GlobalSettings globalSettings,
IReferenceEventService referenceEventService,
IUserService userService,
IProviderRepository providerRepository,
ILogger<OrganizationsController> logger)
{
_organizationService = organizationService;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_organizationConnectionRepository = organizationConnectionRepository;
@ -64,6 +69,7 @@ public class OrganizationsController : Controller
_globalSettings = globalSettings;
_referenceEventService = referenceEventService;
_userService = userService;
_providerRepository = providerRepository;
_logger = logger;
}
@ -103,6 +109,7 @@ public class OrganizationsController : Controller
return RedirectToAction("Index");
}
var provider = await _providerRepository.GetByOrganizationIdAsync(id);
var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(id);
var collections = await _collectionRepository.GetManyByOrganizationIdAsync(id);
IEnumerable<Group> groups = null;
@ -117,7 +124,7 @@ public class OrganizationsController : Controller
}
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
return View(new OrganizationViewModel(organization, billingSyncConnection, users, ciphers, collections, groups, policies));
return View(new OrganizationViewModel(organization, provider, billingSyncConnection, users, ciphers, collections, groups, policies));
}
[SelfHosted(NotSelfHostedOnly = true)]
@ -129,6 +136,7 @@ public class OrganizationsController : Controller
return RedirectToAction("Index");
}
var provider = await _providerRepository.GetByOrganizationIdAsync(id);
var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(id);
var collections = await _collectionRepository.GetManyByOrganizationIdAsync(id);
IEnumerable<Group> groups = null;
@ -144,7 +152,7 @@ public class OrganizationsController : Controller
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
var billingInfo = await _paymentService.GetBillingAsync(organization);
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
return View(new OrganizationEditModel(organization, users, ciphers, collections, groups, policies,
return View(new OrganizationEditModel(organization, provider, users, ciphers, collections, groups, policies,
billingInfo, billingSyncConnection, _globalSettings));
}
@ -214,4 +222,21 @@ public class OrganizationsController : Controller
return RedirectToAction("Index");
}
[HttpPost]
public async Task<IActionResult> ResendOwnerInvite(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
return RedirectToAction("Index");
}
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);
foreach (var organizationUser in organizationUsers)
{
await _organizationService.ResendInviteAsync(id, null, organizationUser.Id, true);
}
return Json(null);
}
}

View File

@ -1,5 +1,7 @@
using Bit.Admin.Models;
using Bit.Core.Entities.Provider;
using Bit.Core.Enums.Provider;
using Bit.Core.Providers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -13,23 +15,42 @@ namespace Bit.Admin.Controllers;
[SelfHosted(NotSelfHostedOnly = true)]
public class ProvidersController : Controller
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IProviderRepository _providerRepository;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly GlobalSettings _globalSettings;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IProviderService _providerService;
private readonly IReferenceEventService _referenceEventService;
private readonly IUserService _userService;
private readonly ICreateProviderCommand _createProviderCommand;
public ProvidersController(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IProviderService providerService,
GlobalSettings globalSettings, IApplicationCacheService applicationCacheService)
public ProvidersController(
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IProviderRepository providerRepository,
IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IProviderService providerService,
GlobalSettings globalSettings,
IApplicationCacheService applicationCacheService,
IReferenceEventService referenceEventService,
IUserService userService,
ICreateProviderCommand createProviderCommand)
{
_organizationRepository = organizationRepository;
_organizationService = organizationService;
_providerRepository = providerRepository;
_providerUserRepository = providerUserRepository;
_providerOrganizationRepository = providerOrganizationRepository;
_providerService = providerService;
_globalSettings = globalSettings;
_applicationCacheService = applicationCacheService;
_referenceEventService = referenceEventService;
_userService = userService;
_createProviderCommand = createProviderCommand;
}
public async Task<IActionResult> Index(string name = null, string userEmail = null, int page = 1, int count = 25)
@ -75,9 +96,18 @@ public class ProvidersController : Controller
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)
@ -130,4 +160,76 @@ public class ProvidersController : Controller
TempData["InviteResentTo"] = ownerId;
return RedirectToAction("Edit", new { id = providerId });
}
[HttpGet]
public async Task<IActionResult> AddExistingOrganization(Guid id, string name = null, string ownerEmail = null, int page = 1, int count = 25)
{
if (page < 1)
{
page = 1;
}
if (count < 1)
{
count = 1;
}
var skip = (page - 1) * count;
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(name, ownerEmail, skip, count);
var viewModel = new OrganizationUnassignedToProviderSearchViewModel
{
OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,
OrganizationOwnerEmail = string.IsNullOrWhiteSpace(ownerEmail) ? null : ownerEmail,
Page = page,
Count = count,
Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel
{
Id = uo.Id,
Name = uo.Name,
PlanType = uo.PlanType
}).ToList()
};
return View(viewModel);
}
[HttpPost]
public async Task<IActionResult> AddExistingOrganization(Guid id, OrganizationUnassignedToProviderSearchViewModel model)
{
var organizationIds = model.Items.Where(o => o.Selected).Select(o => o.Id).ToArray();
if (organizationIds.Any())
{
await _providerService.AddOrganizationsToReseller(id, organizationIds);
}
return RedirectToAction("Edit", "Providers", new { id = id });
}
[HttpGet]
public async Task<IActionResult> CreateOrganization(Guid providerId)
{
var provider = await _providerRepository.GetByIdAsync(providerId);
if (provider is not { Type: ProviderType.Reseller })
{
return RedirectToAction("Index");
}
return View(new OrganizationEditModel(provider));
}
[HttpPost]
public async Task<IActionResult> CreateOrganization(Guid providerId, OrganizationEditModel model)
{
var provider = await _providerRepository.GetByIdAsync(providerId);
if (provider is not { Type: ProviderType.Reseller })
{
return RedirectToAction("Index");
}
var organization = model.CreateOrganization(provider);
await _organizationService.CreatePendingOrganization(organization, model.Owners, User, _userService, model.SalesAssistedTrialStarted);
await _providerService.AddOrganization(providerId, organization.Id, null);
return RedirectToAction("Edit", "Providers", new { id = providerId });
}
}

View File

@ -1,12 +1,59 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities.Provider;
using Bit.Core.Enums.Provider;
using Bit.SharedWeb.Utilities;
namespace Bit.Admin.Models;
public class CreateProviderModel
public class CreateProviderModel : IValidatableObject
{
public CreateProviderModel() { }
[Display(Name = "Provider Type")]
public ProviderType Type { get; set; }
[Display(Name = "Owner Email")]
[Required]
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;
}
}
}

View File

@ -1,11 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities;
using Bit.Core.Entities.Provider;
using Bit.Core.Enums;
using Bit.Core.Enums.Provider;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Bit.SharedWeb.Utilities;
namespace Bit.Admin.Models;
@ -13,18 +16,26 @@ public class OrganizationEditModel : OrganizationViewModel
{
public OrganizationEditModel() { }
public OrganizationEditModel(Organization org, IEnumerable<OrganizationUserUserDetails> orgUsers,
public OrganizationEditModel(Provider provider)
{
Provider = provider;
BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty;
PlanType = Core.Enums.PlanType.TeamsMonthly;
Plan = Core.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName();
}
public OrganizationEditModel(Organization org, Provider provider, IEnumerable<OrganizationUserUserDetails> orgUsers,
IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups,
IEnumerable<Policy> policies, BillingInfo billingInfo, IEnumerable<OrganizationConnection> connections,
GlobalSettings globalSettings)
: base(org, connections, orgUsers, ciphers, collections, groups, policies)
: base(org, provider, connections, orgUsers, ciphers, collections, groups, policies)
{
BillingInfo = billingInfo;
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
Name = org.Name;
BusinessName = org.BusinessName;
BillingEmail = org.BillingEmail;
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
PlanType = org.PlanType;
Plan = org.Plan;
Seats = org.Seats;
@ -60,7 +71,7 @@ public class OrganizationEditModel : OrganizationViewModel
public string BraintreeMerchantId { get; set; }
[Required]
[Display(Name = "Name")]
[Display(Name = "Organization Name")]
public string Name { get; set; }
[Display(Name = "Business Name")]
public string BusinessName { get; set; }
@ -124,6 +135,13 @@ public class OrganizationEditModel : OrganizationViewModel
public DateTime? ExpirationDate { get; set; }
public bool SalesAssistedTrialStarted { get; set; }
public Organization CreateOrganization(Provider provider)
{
BillingEmail = provider.BillingEmail;
return ToOrganization(new Organization());
}
public Organization ToOrganization(Organization existingOrganization)
{
existingOrganization.Name = Name;

View File

@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Admin.Models;
public class OrganizationSelectableViewModel : Organization
{
public bool Selected { get; set; }
}

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Admin.Models;
public class OrganizationUnassignedToProviderSearchViewModel : PagedModel<OrganizationSelectableViewModel>
{
[Display(Name = "Organization Name")]
public string OrganizationName { get; set; }
[Display(Name = "Owner Email")]
public string OrganizationOwnerEmail { get; set; }
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Entities.Provider;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Vault.Entities;
@ -9,11 +10,12 @@ public class OrganizationViewModel
{
public OrganizationViewModel() { }
public OrganizationViewModel(Organization org, IEnumerable<OrganizationConnection> connections,
public OrganizationViewModel(Organization org, Provider provider, IEnumerable<OrganizationConnection> connections,
IEnumerable<OrganizationUserUserDetails> orgUsers, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<Group> groups, IEnumerable<Policy> policies)
{
Organization = org;
Provider = provider;
Connections = connections ?? Enumerable.Empty<OrganizationConnection>();
HasPublicPrivateKeys = org.PublicKey != null && org.PrivateKey != null;
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
@ -24,17 +26,21 @@ public class OrganizationViewModel
CollectionCount = collections.Count();
GroupCount = groups?.Count() ?? 0;
PolicyCount = policies?.Count() ?? 0;
var organizationUserStatus = org.Status == OrganizationStatusType.Pending
? OrganizationUserStatusType.Invited
: OrganizationUserStatusType.Confirmed;
Owners = string.Join(", ",
orgUsers
.Where(u => u.Type == OrganizationUserType.Owner && u.Status == OrganizationUserStatusType.Confirmed)
.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus)
.Select(u => u.Email));
Admins = string.Join(", ",
orgUsers
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == OrganizationUserStatusType.Confirmed)
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
.Select(u => u.Email));
}
public Organization Organization { get; set; }
public Provider Provider { get; set; }
public IEnumerable<OrganizationConnection> Connections { get; set; }
public string Owners { get; set; }
public string Admins { get; set; }

View File

@ -14,10 +14,13 @@ public class ProviderEditModel : ProviderViewModel
Name = provider.Name;
BusinessName = provider.BusinessName;
BillingEmail = provider.BillingEmail;
BillingPhone = provider.BillingPhone;
}
[Display(Name = "Billing Email")]
public string BillingEmail { get; set; }
[Display(Name = "Billing Phone Number")]
public string BillingPhone { get; set; }
[Display(Name = "Business Name")]
public string BusinessName { get; set; }
public string Name { get; set; }
@ -28,6 +31,7 @@ public class ProviderEditModel : ProviderViewModel
existingProvider.Name = Name;
existingProvider.BusinessName = BusinessName;
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
return existingProvider;
}
}

View File

@ -1,21 +1,20 @@
@model OrganizationEditModel
@{
ViewData["Title"] = "Organization: " + Model.Organization.Name;
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Organization.Name;
}
@section Scripts {
@await Html.PartialAsync("_OrganizationFormScripts")
<script>
(() => {
document.getElementById('teams-trial').addEventListener('click', () => {
if (document.getElementById('@(nameof(Model.PlanType))').value !==
'@((byte)Bit.Core.Enums.PlanType.Free)') {
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)Bit.Core.Enums.PlanType.Free)') {
alert('Organization is not on a free plan.');
return;
}
// Plan
document.getElementById('@(nameof(Model.PlanType))').value =
'@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)';
document.getElementById('@(nameof(Model.PlanType))').value = '@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)';
document.getElementById('@(nameof(Model.Plan))').value = 'Teams (Trial)';
document.getElementById('@(nameof(Model.Seats))').value = '10';
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
@ -38,19 +37,14 @@
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
});
document.getElementById('enterprise-trial').addEventListener('click', () => {
if (document.getElementById('@(nameof(Model.PlanType))').value !==
'@((byte)Bit.Core.Enums.PlanType.Free)') {
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)Bit.Core.Enums.PlanType.Free)') {
alert('Organization is not on a free plan.');
return;
}
// Plan
document.getElementById('@(nameof(Model.PlanType))').value =
'@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)';
document.getElementById('@(nameof(Model.PlanType))').value = '@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)';
document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';
document.getElementById('@(nameof(Model.Seats))').value = '10';
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
@ -73,260 +67,24 @@
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
});
document.getElementById('@(nameof(Model.PlanType))').addEventListener('change', () => {
const selectEl = document.getElementById('@(nameof(Model.PlanType))');
const selectText = selectEl.options[selectEl.selectedIndex].text;
document.getElementById('@(nameof(Model.Plan))').value = selectText;
});
document.getElementById('gateway-customer-link').addEventListener('click', () => {
const gateway = document.getElementById('@(nameof(Model.Gateway))');
const customerId = document.getElementById('@(nameof(Model.GatewayCustomerId))');
if (!gateway || gateway.value === '' || !customerId || customerId.value === '') {
return;
}
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
window.open('https://dashboard.stripe.com/customers/' + customerId.value, '_blank');
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/'
+ customerId.value, '_blank');
}
});
document.getElementById('gateway-subscription-link').addEventListener('click', () => {
const gateway = document.getElementById('@(nameof(Model.Gateway))');
const subId = document.getElementById('@(nameof(Model.GatewaySubscriptionId))');
if (!gateway || gateway.value === '' || !subId || subId.value === '') {
return;
}
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
window.open('https://dashboard.stripe.com/subscriptions/' + subId.value, '_blank');
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/' +
'subscriptions/' + subId.value, '_blank');
}
});
})();
</script>
}
<h1>Organization <small>@Model.Organization.Name</small></h1>
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Organization.Name</small></h1>
@if (Model.Provider != null)
{
<h2>Provider Relationship</h2>
@await Html.PartialAsync("_ProviderInformation", Model.Provider)
}
<h2>Organization Information</h2>
@await Html.PartialAsync("_ViewInformation", Model)
<h2>Billing Information</h2>
@await Html.PartialAsync("_BillingInformation",
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id })
<form method="post" id="edit-form">
<input asp-for="SalesAssistedTrialStarted" type="hidden">
<h2>General</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" class="form-control" asp-for="Name" required>
</div>
</div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" asp-for="Enabled">
<label class="form-check-label" asp-for="Enabled"></label>
</div>
<h2>Business Information</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="BusinessName"></label>
<input type="text" class="form-control" asp-for="BusinessName">
</div>
</div>
</div>
<h2>Plan</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="PlanType"></label>
<select class="form-control" asp-for="PlanType"
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.PlanType>()"></select>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="Plan"></label>
<input type="text" class="form-control" asp-for="Plan" required>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="Seats"></label>
<input type="number" class="form-control" asp-for="Seats" min="1">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="MaxCollections"></label>
<input type="number" class="form-control" asp-for="MaxCollections" min="1">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="MaxStorageGb"></label>
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1">
</div>
</div>
</div>
<div class="row">
<div class="col-4">
<div class="form-group">
<label asp-for="MaxAutoscaleSeats"></label>
<input type="number" class="form-control" asp-for="MaxAutoscaleSeats" min="1">
</div>
</div>
</div>
<h2>Features</h2>
<div class="row mb-3">
<div class="col-4">
<h3>General</h3>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" asp-for="SelfHost">
<label class="form-check-label" asp-for="SelfHost"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="Use2fa">
<label class="form-check-label" asp-for="Use2fa"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseApi">
<label class="form-check-label" asp-for="UseApi"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseGroups">
<label class="form-check-label" asp-for="UseGroups"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UsePolicies">
<label class="form-check-label" asp-for="UsePolicies"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseSso">
<label class="form-check-label" asp-for="UseSso"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
<label class="form-check-label" asp-for="UseKeyConnector"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseScim">
<label class="form-check-label" asp-for="UseScim"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
<label class="form-check-label" asp-for="UseDirectory"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseEvents">
<label class="form-check-label" asp-for="UseEvents"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseResetPassword">
<label class="form-check-label" asp-for="UseResetPassword"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions">
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
</div>
</div>
<div class="col-4">
<h3>Password Manager</h3>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseTotp">
<label class="form-check-label" asp-for="UseTotp"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UsersGetPremium">
<label class="form-check-label" asp-for="UsersGetPremium"></label>
</div>
</div>
<div class="col-4">
<h3>Secrets Manager</h3>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseSecretsManager">
<label class="form-check-label" asp-for="UseSecretsManager"></label>
</div>
</div>
</div>
<h2>Licensing</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="LicenseKey"></label>
<input type="text" class="form-control" asp-for="LicenseKey">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="ExpirationDate"></label>
<input type="datetime-local" class="form-control" asp-for="ExpirationDate">
</div>
</div>
</div>
<h2>Billing</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="BillingEmail"></label>
<input type="email" class="form-control" asp-for="BillingEmail">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<div class="form-group">
<label asp-for="Gateway"></label>
<select class="form-control" asp-for="Gateway"
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
<option value="">--</option>
</select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewayCustomerId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewayCustomerId">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
<i class="fa fa-external-link"></i>
</button>
</div>
</div>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewaySubscriptionId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
<i class="fa fa-external-link"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</form>
@await Html.PartialAsync("_OrganizationForm", Model)
<div class="d-flex mt-4">
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
<div class="ml-auto d-flex">

View File

@ -6,6 +6,11 @@
<h1>Organization <small>@Model.Organization.Name</small></h1>
@if (Model.Provider != null)
{
<h2>Provider Relationship</h2>
@await Html.PartialAsync("_ProviderInformation", Model.Provider)
}
<h2>Information</h2>
@await Html.PartialAsync("_ViewInformation", Model)
@if(GlobalSettings.SelfHosted)

View File

@ -0,0 +1,9 @@
@using Bit.SharedWeb.Utilities
@model Bit.Core.Entities.Provider.Provider
<dl class="row">
<dt class="col-sm-4 col-lg-3">Provider Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Name</dd>
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
<dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>
</dl>

View File

@ -0,0 +1,97 @@
@using Bit.SharedWeb.Utilities
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model OrganizationUnassignedToProviderSearchViewModel
@{
ViewData["Title"] = "Add Existing Organization";
var providerId = ViewContext.RouteData.Values["id"];
}
<h1>Add Existing Organization</h1>
<div class="row mb-2">
<div class="col">
<form class="form-inline mb-2" method="get" asp-route-id="@providerId">
<label class="sr-only" asp-for="OrganizationName"></label>
<input type="text" class="form-control mb-2 mr-2 flex-fill" placeholder="@Html.DisplayNameFor(m => m.OrganizationName)" asp-for="OrganizationName" name="name">
<label class="sr-only" asp-for="OrganizationOwnerEmail"></label>
<input type="email" class="form-control mb-2 mr-2 flex-fill" placeholder="@Html.DisplayNameFor(m => m.OrganizationOwnerEmail)" asp-for="OrganizationOwnerEmail" name="ownerEmail">
<button type="submit" class="btn btn-primary mb-2" title="Search" formmethod="get"><i class="fa fa-search"></i> Search</button>
</form>
</div>
</div>
<form method="post" id="select-form" asp-route-id="@providerId">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 20px;">All</th>
<th>Name</th>
<th style="width: 190px;">Plan</th>
</tr>
</thead>
<tbody>
@if (!Model.Items.Any())
{
<tr>
<td colspan="5">No results to list.</td>
</tr>
}
else
{
@for (var i = 0; i < Model.Items.Count; i++)
{
<tr>
<td class="text-center">
@Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = "readonly", autocomplete = "off" })
@Html.CheckBoxFor(m => Model.Items[i].Selected)
</td>
<td>@Html.ActionLink(Model.Items[i].Name, "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
<td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td>
</tr>
}
}
</tbody>
</table>
</div>
</form>
<div class="row">
<div class="col">
<nav>
<ul class="pagination">
@if (Model.PreviousPage.HasValue)
{
<li class="page-item">
<a class="page-link" asp-action="AddExistingOrganization" asp-route-id="@providerId" asp-route-page="@Model.PreviousPage.Value"
asp-route-count="@Model.Count" asp-route-ownerEmail="@Model.OrganizationOwnerEmail"
asp-route-name="@Model.OrganizationName">Previous</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
}
@if (Model.NextPage.HasValue)
{
<li class="page-item">
<a class="page-link" asp-action="AddExistingOrganization" asp-route-id="@providerId" asp-route-page="@Model.NextPage.Value"
asp-route-count="@Model.Count" asp-route-ownerEmail="@Model.OrganizationOwnerEmail"
asp-route-name="@Model.OrganizationName">Next</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Next</a>
</li>
}
</ul>
</nav>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary" form="select-form">Add to Reseller</button>
</div>
</div>

View File

@ -1,16 +1,55 @@
@model CreateProviderModel
@using Bit.SharedWeb.Utilities
@model CreateProviderModel
@{
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>
<form method="post">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<div class="form-group">
<label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
<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">
<label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
</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>

View File

@ -0,0 +1,21 @@
@model OrganizationEditModel
@{
ViewData["Title"] = "Create Client Organization";
}
@section Scripts {
@await Html.PartialAsync("_OrganizationFormScripts")
}
<h1>New Client Organization</h1>
@await Html.PartialAsync("_OrganizationForm", Model)
<div class="d-flex mt-4">
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
<div class="ml-auto d-flex">
<form asp-controller="Providers" asp-action="Edit" asp-route-id="@Model.Provider.Id"
onsubmit="return confirm('Are you sure you want to cancel?')">
<button class="btn btn-outline-secondary" type="submit">Cancel</button>
</form>
</div>
</div>

View File

@ -36,6 +36,14 @@
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="BillingPhone"></label>
<input type="tel" class="form-control" asp-for="BillingPhone">
</div>
</div>
</div>
</form>
@await Html.PartialAsync("Organizations", Model)
<div class="d-flex mt-4">

View File

@ -1,4 +1,5 @@
@model ProvidersModel
@using Bit.SharedWeb.Utilities
@model ProvidersModel
@{
ViewData["Title"] = "Providers";
}
@ -25,6 +26,7 @@
<thead>
<tr>
<th>Name</th>
<th style="width: 190px;">Provider Type</th>
<th style="width: 190px;">Status</th>
<th style="width: 150px;">Created</th>
</tr>
@ -44,6 +46,7 @@
<td>
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
</td>
<td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
<td>@provider.Status</td>
<td>
<span title="@provider.CreationDate.ToString()">

View File

@ -1,4 +1,7 @@
@model ProviderViewModel
@await Html.PartialAsync("_ProviderScripts")
<h2>Provider Organizations</h2>
<div class="row">
<div class="col-sm">
@ -6,7 +9,17 @@
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th style="width: 50%;">Name</th>
<th style="width: 50%;">Status</th>
<th>
@if (Model.Provider.Type == ProviderType.Reseller)
{
<div class="float-right text-nowrap">
<a asp-controller="Providers" asp-action="CreateOrganization" asp-route-providerId="@Model.Provider.Id" class="btn btn-sm btn-primary">New Organization</a>
<a asp-controller="Providers" asp-action="AddExistingOrganization" asp-route-id="@Model.Provider.Id" class="btn btn-sm btn-outline-primary">Add Existing Organization</a>
</div>
}
</th>
</tr>
</thead>
<tbody>
@ -22,8 +35,24 @@
{
<tr>
<td class="align-middle">
<a asp-controller="Organizations" asp-action="Edit"
asp-route-id="@org.OrganizationId">@org.OrganizationName</a>
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@org.OrganizationId">@org.OrganizationName</a>
</td>
<td>
@org.Status
</td>
<td>
<div class="float-right">
@if (org.Status == OrganizationStatusType.Pending)
{
<a href="#" class="float-right" onclick="return resendOwnerInvite('@org.OrganizationId', '@org.OrganizationName');">
<i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i>
</a>
}
else
{
<i class="fa fa-envelope-o fa-lg text-secondary"></i>
}
</div>
</td>
</tr>
}

View File

@ -0,0 +1,20 @@
<script>
function resendOwnerInvite(orgId, orgName) {
if (confirm('Resend invite to "' + orgName + '"?')) {
$.ajax({
type: "POST",
url: '@Url.Action("ResendOwnerInvite", "Organizations")' + '?id=' + orgId,
dataType: 'json',
contentType: false,
processData: false,
success: function (response) {
alert('Invitation has been resent!');
},
error: function (response) {
alert("Error!");
}
});
}
return false;
}
</script>

View File

@ -1,4 +1,5 @@
@model ProviderViewModel
@using Bit.SharedWeb.Utilities
@model ProviderViewModel
<dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt>
<dd class="col-sm-8 col-lg-9"><code>@Model.Provider.Id</code></dd>
@ -7,7 +8,10 @@
<dd class="col-sm-8 col-lg-9">@Model.Provider.Status</dd>
<dt class="col-sm-4 col-lg-3">Users</dt>
<dd class="col-sm-8 col-lg-9">@Model.UserCount</dd>
<dd class="col-sm-8 col-lg-9">@(Model.Provider.Type == ProviderType.Reseller ? "N/A" : Model.UserCount)</dd>
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
<dd class="col-sm-8 col-lg-9">@(Model.Provider.Type.GetDisplayAttribute()?.GetName())</dd>
<dt class="col-sm-4 col-lg-3">Created</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.CreationDate.ToString()</dd>

View File

@ -0,0 +1,247 @@
@using Bit.SharedWeb.Utilities
@model OrganizationEditModel
<form method="post" id="edit-form" asp-route-providerId="@Model.Provider?.Id">
<input asp-for="SalesAssistedTrialStarted" type="hidden">
<h2>General</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" class="form-control" asp-for="Name" required>
</div>
</div>
</div>
@if (Model.Provider?.Type == ProviderType.Reseller)
{
<div class="row">
<div class="col-sm">
<div class="form-group">
<label>Client Owner Email</label>
@if (!string.IsNullOrWhiteSpace(Model.Owners))
{
<input type="text" class="form-control" asp-for="Owners" readonly="readonly">
}
else
{
<input type="text" class="form-control" asp-for="Owners" required>
}
<label class="form-check-label small text-muted align-top">This user should be independent of the Provider. If the Provider is disassociated with the organization, this user will maintain ownership of the organization.</label>
</div>
</div>
</div>
}
@if (Model.Organization != null)
{
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" asp-for="Enabled">
<label class="form-check-label" asp-for="Enabled"></label>
</div>
}
<h2>Business Information</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="BusinessName"></label>
<input type="text" class="form-control" asp-for="BusinessName">
</div>
</div>
</div>
<h2>Plan</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="PlanType"></label>
@{
var planTypes = Enum.GetValues<PlanType>()
.Where(p => Model.Provider == null || p is >= PlanType.TeamsMonthly and <= PlanType.EnterpriseAnnually)
.Select(e => new SelectListItem
{
Value = ((int)e).ToString(),
Text = e.GetDisplayAttribute()?.GetName() ?? e.ToString()
})
.ToList();
}
<select class="form-control" asp-for="PlanType" asp-items="planTypes"></select>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="Plan"></label>
<input type="text" class="form-control" asp-for="Plan" required>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="Seats"></label>
<input type="number" class="form-control" asp-for="Seats" min="1">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="MaxCollections"></label>
<input type="number" class="form-control" asp-for="MaxCollections" min="1">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="MaxStorageGb"></label>
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1">
</div>
</div>
</div>
<div class="row">
<div class="col-4">
<div class="form-group">
<label asp-for="MaxAutoscaleSeats"></label>
<input type="number" class="form-control" asp-for="MaxAutoscaleSeats" min="1">
</div>
</div>
</div>
<h2>Features</h2>
<div class="row mb-3">
<div class="col-4">
<h3>General</h3>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" asp-for="SelfHost">
<label class="form-check-label" asp-for="SelfHost"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="Use2fa">
<label class="form-check-label" asp-for="Use2fa"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseApi">
<label class="form-check-label" asp-for="UseApi"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseGroups">
<label class="form-check-label" asp-for="UseGroups"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UsePolicies">
<label class="form-check-label" asp-for="UsePolicies"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseSso">
<label class="form-check-label" asp-for="UseSso"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
<label class="form-check-label" asp-for="UseKeyConnector"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseScim">
<label class="form-check-label" asp-for="UseScim"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
<label class="form-check-label" asp-for="UseDirectory"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseEvents">
<label class="form-check-label" asp-for="UseEvents"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseResetPassword">
<label class="form-check-label" asp-for="UseResetPassword"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions">
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
</div>
</div>
<div class="col-4">
<h3>Password Manager</h3>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseTotp">
<label class="form-check-label" asp-for="UseTotp"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UsersGetPremium">
<label class="form-check-label" asp-for="UsersGetPremium"></label>
</div>
</div>
<div class="col-4">
<h3>Secrets Manager</h3>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseSecretsManager">
<label class="form-check-label" asp-for="UseSecretsManager"></label>
</div>
</div>
</div>
<h2>Licensing</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="LicenseKey"></label>
<input type="text" class="form-control" asp-for="LicenseKey">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="ExpirationDate"></label>
<input type="datetime-local" class="form-control" asp-for="ExpirationDate">
</div>
</div>
</div>
<h2>Billing</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="BillingEmail"></label>
@if (Model.Provider?.Type == ProviderType.Reseller)
{
<input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
}
else
{
<input type="email" class="form-control" asp-for="BillingEmail">
}
</div>
</div>
<div class="col-sm">
<div class="form-group">
<div class="form-group">
<label asp-for="Gateway"></label>
<select class="form-control" asp-for="Gateway"
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
<option value="">--</option>
</select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewayCustomerId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewayCustomerId">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
<i class="fa fa-external-link"></i>
</button>
</div>
</div>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewaySubscriptionId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
<i class="fa fa-external-link"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,35 @@
<script>
(() => {
document.getElementById('@(nameof(Model.PlanType))').addEventListener('change', () => {
const selectEl = document.getElementById('@(nameof(Model.PlanType))');
const selectText = selectEl.options[selectEl.selectedIndex].text;
document.getElementById('@(nameof(Model.Plan))').value = selectText;
});
document.getElementById('gateway-customer-link').addEventListener('click', () => {
const gateway = document.getElementById('@(nameof(Model.Gateway))');
const customerId = document.getElementById('@(nameof(Model.GatewayCustomerId))');
if (!gateway || gateway.value === '' || !customerId || customerId.value === '') {
return;
}
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
window.open('https://dashboard.stripe.com/customers/' + customerId.value, '_blank');
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/'
+ customerId.value, '_blank');
}
});
document.getElementById('gateway-subscription-link').addEventListener('click', () => {
const gateway = document.getElementById('@(nameof(Model.Gateway))');
const subId = document.getElementById('@(nameof(Model.GatewaySubscriptionId))');
if (!gateway || gateway.value === '' || !subId || subId.value === '') {
return;
}
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
window.open('https://dashboard.stripe.com/subscriptions/' + subId.value, '_blank');
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/' +
'subscriptions/' + subId.value, '_blank');
}
});
})();
</script>