mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 00:22:50 -05:00
Merge branch 'main' into ac/ac-2323/sql-automatic-data-migrations
This commit is contained in:
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@ -44,6 +44,7 @@
|
|||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
"AspNetCoreRateLimit",
|
"AspNetCoreRateLimit",
|
||||||
"AspNetCoreRateLimit.Redis",
|
"AspNetCoreRateLimit.Redis",
|
||||||
|
"Azure.Data.Tables",
|
||||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||||
"Azure.Messaging.EventGrid",
|
"Azure.Messaging.EventGrid",
|
||||||
"Azure.Messaging.ServiceBus",
|
"Azure.Messaging.ServiceBus",
|
||||||
@ -53,7 +54,6 @@
|
|||||||
"Fido2.AspNet",
|
"Fido2.AspNet",
|
||||||
"Duende.IdentityServer",
|
"Duende.IdentityServer",
|
||||||
"Microsoft.Azure.Cosmos",
|
"Microsoft.Azure.Cosmos",
|
||||||
"Microsoft.Azure.Cosmos.Table",
|
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||||
"Microsoft.Extensions.Identity.Stores",
|
"Microsoft.Extensions.Identity.Stores",
|
||||||
"Otp.NET",
|
"Otp.NET",
|
||||||
|
14
.github/workflows/scan.yml
vendored
14
.github/workflows/scan.yml
vendored
@ -10,8 +10,6 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-run:
|
check-run:
|
||||||
name: Check PR run
|
name: Check PR run
|
||||||
@ -22,6 +20,8 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: check-run
|
needs: check-run
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -40,10 +40,13 @@ jobs:
|
|||||||
base_uri: https://ast.checkmarx.net/
|
base_uri: https://ast.checkmarx.net/
|
||||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||||
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
|
additional_params: |
|
||||||
|
--report-format sarif \
|
||||||
|
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||||
|
--output-path . ${{ env.INCREMENTAL }}
|
||||||
|
|
||||||
- name: Upload Checkmarx results to GitHub
|
- name: Upload Checkmarx results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||||
with:
|
with:
|
||||||
sarif_file: cx_result.sarif
|
sarif_file: cx_result.sarif
|
||||||
|
|
||||||
@ -51,6 +54,9 @@ jobs:
|
|||||||
name: Quality scan
|
name: Quality scan
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: check-run
|
needs: check-run
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2024.3.1</Version>
|
<Version>2024.4.0</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Billing.Repositories;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
|
|
||||||
@ -14,21 +19,28 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public CreateProviderCommand(
|
public CreateProviderCommand(
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
IUserRepository userRepository)
|
IUserRepository userRepository,
|
||||||
|
IProviderPlanRepository providerPlanRepository,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
|
_providerPlanRepository = providerPlanRepository;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateMspAsync(Provider provider, string ownerEmail)
|
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
||||||
{
|
{
|
||||||
|
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||||
if (owner == null)
|
if (owner == null)
|
||||||
{
|
{
|
||||||
@ -44,8 +56,24 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
Type = ProviderUserType.ProviderAdmin,
|
Type = ProviderUserType.ProviderAdmin,
|
||||||
Status = ProviderUserStatusType.Confirmed,
|
Status = ProviderUserStatusType.Confirmed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isConsolidatedBillingEnabled)
|
||||||
|
{
|
||||||
|
var providerPlans = new List<ProviderPlan>
|
||||||
|
{
|
||||||
|
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
|
||||||
|
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var providerPlan in providerPlans)
|
||||||
|
{
|
||||||
|
await _providerPlanRepository.CreateAsync(providerPlan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _providerUserRepository.CreateAsync(providerUser);
|
await _providerUserRepository.CreateAsync(providerUser);
|
||||||
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateResellerAsync(Provider provider)
|
public async Task CreateResellerAsync(Provider provider)
|
||||||
@ -60,4 +88,16 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
provider.UseEvents = true;
|
provider.UseEvents = true;
|
||||||
await _providerRepository.CreateAsync(provider);
|
await _providerRepository.CreateAsync(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum)
|
||||||
|
{
|
||||||
|
return new ProviderPlan
|
||||||
|
{
|
||||||
|
ProviderId = providerId,
|
||||||
|
PlanType = planType,
|
||||||
|
SeatMinimum = seatMinimum,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -382,10 +382,14 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
organization.BillingEmail = provider.BillingEmail;
|
organization.BillingEmail = provider.BillingEmail;
|
||||||
await _organizationRepository.ReplaceAsync(organization);
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
|
||||||
|
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
Email = provider.BillingEmail
|
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||||
});
|
{
|
||||||
|
Email = provider.BillingEmail
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,6 @@
|
|||||||
"connectionString": "SECRET",
|
"connectionString": "SECRET",
|
||||||
"applicationCacheTopicName": "SECRET"
|
"applicationCacheTopicName": "SECRET"
|
||||||
},
|
},
|
||||||
"documentDb": {
|
|
||||||
"uri": "SECRET",
|
|
||||||
"key": "SECRET"
|
|
||||||
},
|
|
||||||
"sentry": {
|
"sentry": {
|
||||||
"dsn": "SECRET"
|
"dsn": "SECRET"
|
||||||
},
|
},
|
||||||
@ -58,6 +54,5 @@
|
|||||||
"region": "SECRET"
|
"region": "SECRET"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scimSettings": {
|
"scimSettings": {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -31,10 +31,6 @@
|
|||||||
"connectionString": "SECRET",
|
"connectionString": "SECRET",
|
||||||
"applicationCacheTopicName": "SECRET"
|
"applicationCacheTopicName": "SECRET"
|
||||||
},
|
},
|
||||||
"documentDb": {
|
|
||||||
"uri": "SECRET",
|
|
||||||
"key": "SECRET"
|
|
||||||
},
|
|
||||||
"notificationHub": {
|
"notificationHub": {
|
||||||
"connectionString": "SECRET",
|
"connectionString": "SECRET",
|
||||||
"hubName": "SECRET"
|
"hubName": "SECRET"
|
||||||
|
@ -22,7 +22,7 @@ public class CreateProviderCommandTests
|
|||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.CreateMspAsync(provider, default));
|
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
|
||||||
Assert.Contains("Invalid owner.", exception.Message);
|
Assert.Contains("Invalid owner.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ public class CreateProviderCommandTests
|
|||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||||
|
|
||||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email);
|
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||||
|
@ -8,6 +8,8 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
|||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@ -34,6 +36,7 @@ public class ProvidersController : Controller
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly ICreateProviderCommand _createProviderCommand;
|
private readonly ICreateProviderCommand _createProviderCommand;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||||
|
|
||||||
public ProvidersController(
|
public ProvidersController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -47,7 +50,8 @@ public class ProvidersController : Controller
|
|||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ICreateProviderCommand createProviderCommand,
|
ICreateProviderCommand createProviderCommand,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService,
|
||||||
|
IProviderPlanRepository providerPlanRepository)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
@ -61,6 +65,7 @@ public class ProvidersController : Controller
|
|||||||
_userService = userService;
|
_userService = userService;
|
||||||
_createProviderCommand = createProviderCommand;
|
_createProviderCommand = createProviderCommand;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_providerPlanRepository = providerPlanRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.Provider_List_View)]
|
[RequirePermission(Permission.Provider_List_View)]
|
||||||
@ -90,11 +95,13 @@ public class ProvidersController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult Create(string ownerEmail = null)
|
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
||||||
{
|
{
|
||||||
return View(new CreateProviderModel
|
return View(new CreateProviderModel
|
||||||
{
|
{
|
||||||
OwnerEmail = ownerEmail
|
OwnerEmail = ownerEmail,
|
||||||
|
TeamsMinimumSeats = teamsMinimumSeats,
|
||||||
|
EnterpriseMinimumSeats = enterpriseMinimumSeats
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +119,8 @@ public class ProvidersController : Controller
|
|||||||
switch (provider.Type)
|
switch (provider.Type)
|
||||||
{
|
{
|
||||||
case ProviderType.Msp:
|
case ProviderType.Msp:
|
||||||
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail);
|
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail, model.TeamsMinimumSeats,
|
||||||
|
model.EnterpriseMinimumSeats);
|
||||||
break;
|
break;
|
||||||
case ProviderType.Reseller:
|
case ProviderType.Reseller:
|
||||||
await _createProviderCommand.CreateResellerAsync(provider);
|
await _createProviderCommand.CreateResellerAsync(provider);
|
||||||
@ -139,6 +147,7 @@ public class ProvidersController : Controller
|
|||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IActionResult> Edit(Guid id)
|
public async Task<IActionResult> Edit(Guid id)
|
||||||
{
|
{
|
||||||
|
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
var provider = await _providerRepository.GetByIdAsync(id);
|
var provider = await _providerRepository.GetByIdAsync(id);
|
||||||
if (provider == null)
|
if (provider == null)
|
||||||
{
|
{
|
||||||
@ -147,7 +156,12 @@ public class ProvidersController : Controller
|
|||||||
|
|
||||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||||
return View(new ProviderEditModel(provider, users, providerOrganizations));
|
if (isConsolidatedBillingEnabled)
|
||||||
|
{
|
||||||
|
var providerPlan = await _providerPlanRepository.GetByProviderId(id);
|
||||||
|
return View(new ProviderEditModel(provider, users, providerOrganizations, providerPlan));
|
||||||
|
}
|
||||||
|
return View(new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -156,6 +170,8 @@ public class ProvidersController : Controller
|
|||||||
[RequirePermission(Permission.Provider_Edit)]
|
[RequirePermission(Permission.Provider_Edit)]
|
||||||
public async Task<IActionResult> Edit(Guid id, ProviderEditModel model)
|
public async Task<IActionResult> Edit(Guid id, ProviderEditModel model)
|
||||||
{
|
{
|
||||||
|
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
|
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||||
var provider = await _providerRepository.GetByIdAsync(id);
|
var provider = await _providerRepository.GetByIdAsync(id);
|
||||||
if (provider == null)
|
if (provider == null)
|
||||||
{
|
{
|
||||||
@ -165,6 +181,15 @@ public class ProvidersController : Controller
|
|||||||
model.ToProvider(provider);
|
model.ToProvider(provider);
|
||||||
await _providerRepository.ReplaceAsync(provider);
|
await _providerRepository.ReplaceAsync(provider);
|
||||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||||
|
if (isConsolidatedBillingEnabled)
|
||||||
|
{
|
||||||
|
model.ToProviderPlan(providerPlans);
|
||||||
|
foreach (var providerPlan in providerPlans)
|
||||||
|
{
|
||||||
|
await _providerPlanRepository.ReplaceAsync(providerPlan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,12 @@ public class CreateProviderModel : IValidatableObject
|
|||||||
[Display(Name = "Primary Billing Email")]
|
[Display(Name = "Primary Billing Email")]
|
||||||
public string BillingEmail { get; set; }
|
public string BillingEmail { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Teams minimum seats")]
|
||||||
|
public int TeamsMinimumSeats { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Enterprise minimum seats")]
|
||||||
|
public int EnterpriseMinimumSeats { get; set; }
|
||||||
|
|
||||||
public virtual Provider ToProvider()
|
public virtual Provider ToProvider()
|
||||||
{
|
{
|
||||||
return new Provider()
|
return new Provider()
|
||||||
@ -45,6 +51,16 @@ public class CreateProviderModel : IValidatableObject
|
|||||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||||
}
|
}
|
||||||
|
if (TeamsMinimumSeats < 0)
|
||||||
|
{
|
||||||
|
var teamsMinimumSeatsDisplayName = nameof(TeamsMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMinimumSeats);
|
||||||
|
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMinimumSeats < 0)
|
||||||
|
{
|
||||||
|
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||||
|
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ProviderType.Reseller:
|
case ProviderType.Reseller:
|
||||||
if (string.IsNullOrWhiteSpace(Name))
|
if (string.IsNullOrWhiteSpace(Name))
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Admin.AdminConsole.Models;
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
@ -8,13 +10,16 @@ public class ProviderEditModel : ProviderViewModel
|
|||||||
{
|
{
|
||||||
public ProviderEditModel() { }
|
public ProviderEditModel() { }
|
||||||
|
|
||||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers,
|
||||||
|
IEnumerable<ProviderOrganizationOrganizationDetails> organizations, IEnumerable<ProviderPlan> providerPlans)
|
||||||
: base(provider, providerUsers, organizations)
|
: base(provider, providerUsers, organizations)
|
||||||
{
|
{
|
||||||
Name = provider.DisplayName();
|
Name = provider.DisplayName();
|
||||||
BusinessName = provider.DisplayBusinessName();
|
BusinessName = provider.DisplayBusinessName();
|
||||||
BillingEmail = provider.BillingEmail;
|
BillingEmail = provider.BillingEmail;
|
||||||
BillingPhone = provider.BillingPhone;
|
BillingPhone = provider.BillingPhone;
|
||||||
|
TeamsMinimumSeats = GetMinimumSeats(providerPlans, PlanType.TeamsMonthly);
|
||||||
|
EnterpriseMinimumSeats = GetMinimumSeats(providerPlans, PlanType.EnterpriseMonthly);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Display(Name = "Billing Email")]
|
[Display(Name = "Billing Email")]
|
||||||
@ -24,12 +29,38 @@ public class ProviderEditModel : ProviderViewModel
|
|||||||
[Display(Name = "Business Name")]
|
[Display(Name = "Business Name")]
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
[Display(Name = "Teams minimum seats")]
|
||||||
|
public int TeamsMinimumSeats { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Enterprise minimum seats")]
|
||||||
|
public int EnterpriseMinimumSeats { get; set; }
|
||||||
[Display(Name = "Events")]
|
[Display(Name = "Events")]
|
||||||
|
|
||||||
|
public IEnumerable<ProviderPlan> ToProviderPlan(IEnumerable<ProviderPlan> existingProviderPlans)
|
||||||
|
{
|
||||||
|
var providerPlans = existingProviderPlans.ToList();
|
||||||
|
foreach (var existingProviderPlan in providerPlans)
|
||||||
|
{
|
||||||
|
existingProviderPlan.SeatMinimum = existingProviderPlan.PlanType switch
|
||||||
|
{
|
||||||
|
PlanType.TeamsMonthly => TeamsMinimumSeats,
|
||||||
|
PlanType.EnterpriseMonthly => EnterpriseMinimumSeats,
|
||||||
|
_ => existingProviderPlan.SeatMinimum
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return providerPlans;
|
||||||
|
}
|
||||||
|
|
||||||
public Provider ToProvider(Provider existingProvider)
|
public Provider ToProvider(Provider existingProvider)
|
||||||
{
|
{
|
||||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
|
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
|
||||||
return existingProvider;
|
return existingProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private int GetMinimumSeats(IEnumerable<ProviderPlan> providerPlans, PlanType planType)
|
||||||
|
{
|
||||||
|
return (from providerPlan in providerPlans where providerPlan.PlanType == planType select (int)providerPlan.SeatMinimum).FirstOrDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
@using Bit.SharedWeb.Utilities
|
@using Bit.SharedWeb.Utilities
|
||||||
@using Bit.Core.AdminConsole.Enums.Provider
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
|
@using Bit.Core
|
||||||
@model CreateProviderModel
|
@model CreateProviderModel
|
||||||
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Create Provider";
|
ViewData["Title"] = "Create Provider";
|
||||||
}
|
}
|
||||||
@ -39,6 +41,23 @@
|
|||||||
<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>
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="TeamsMinimumSeats"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="TeamsMinimumSeats">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
@using Bit.Admin.Enums;
|
@using Bit.Admin.Enums;
|
||||||
|
@using Bit.Core
|
||||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||||
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
|
||||||
@model ProviderEditModel
|
@model ProviderEditModel
|
||||||
@{
|
@{
|
||||||
@ -41,6 +43,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="TeamsMinimumSeats"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="TeamsMinimumSeats">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</form>
|
</form>
|
||||||
@await Html.PartialAsync("Organizations", Model)
|
@await Html.PartialAsync("Organizations", Model)
|
||||||
@if (canEdit)
|
@if (canEdit)
|
||||||
|
@ -76,14 +76,18 @@ public class JobsHostedService : BaseJobsHostedService
|
|||||||
{
|
{
|
||||||
new Tuple<Type, ITrigger>(typeof(DeleteSendsJob), everyFiveMinutesTrigger),
|
new Tuple<Type, ITrigger>(typeof(DeleteSendsJob), everyFiveMinutesTrigger),
|
||||||
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger),
|
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger),
|
||||||
new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger),
|
|
||||||
new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger),
|
|
||||||
new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc),
|
new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc),
|
||||||
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger),
|
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger),
|
||||||
new Tuple<Type, ITrigger>(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger),
|
new Tuple<Type, ITrigger>(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger),
|
||||||
new Tuple<Type, ITrigger>(typeof(DeleteUnverifiedOrganizationDomainsJob), everyDayAtTwoAmUtcTrigger),
|
new Tuple<Type, ITrigger>(typeof(DeleteUnverifiedOrganizationDomainsJob), everyDayAtTwoAmUtcTrigger),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!(_globalSettings.SqlServer?.DisableDatabaseMaintenanceJobs ?? false))
|
||||||
|
{
|
||||||
|
jobs.Add(new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger));
|
||||||
|
jobs.Add(new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger));
|
||||||
|
}
|
||||||
|
|
||||||
if (!_globalSettings.SelfHosted)
|
if (!_globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
jobs.Add(new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger));
|
jobs.Add(new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger));
|
||||||
|
@ -88,7 +88,7 @@ public class Startup
|
|||||||
services.AddBaseServices(globalSettings);
|
services.AddBaseServices(globalSettings);
|
||||||
services.AddDefaultServices(globalSettings);
|
services.AddDefaultServices(globalSettings);
|
||||||
services.AddScoped<IAccessControlService, AccessControlService>();
|
services.AddScoped<IAccessControlService, AccessControlService>();
|
||||||
services.AddBillingCommands();
|
services.AddBillingOperations();
|
||||||
|
|
||||||
#if OSS
|
#if OSS
|
||||||
services.AddOosServices();
|
services.AddOosServices();
|
||||||
|
@ -30,10 +30,6 @@
|
|||||||
"connectionString": "SECRET",
|
"connectionString": "SECRET",
|
||||||
"applicationCacheTopicName": "SECRET"
|
"applicationCacheTopicName": "SECRET"
|
||||||
},
|
},
|
||||||
"documentDb": {
|
|
||||||
"uri": "SECRET",
|
|
||||||
"key": "SECRET"
|
|
||||||
},
|
|
||||||
"notificationHub": {
|
"notificationHub": {
|
||||||
"connectionString": "SECRET",
|
"connectionString": "SECRET",
|
||||||
"hubName": "SECRET"
|
"hubName": "SECRET"
|
||||||
|
@ -80,7 +80,6 @@ public class OrganizationDomainController : Controller
|
|||||||
var organizationDomain = new OrganizationDomain
|
var organizationDomain = new OrganizationDomain
|
||||||
{
|
{
|
||||||
OrganizationId = orgId,
|
OrganizationId = orgId,
|
||||||
Txt = model.Txt,
|
|
||||||
DomainName = model.DomainName.ToLower()
|
DomainName = model.DomainName.ToLower()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
||||||
private readonly IGetSubscriptionQuery _getSubscriptionQuery;
|
private readonly ISubscriberQueries _subscriberQueries;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ public class OrganizationsController : Controller
|
|||||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
||||||
IPushNotificationService pushNotificationService,
|
IPushNotificationService pushNotificationService,
|
||||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||||
IGetSubscriptionQuery getSubscriptionQuery,
|
ISubscriberQueries subscriberQueries,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand)
|
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand)
|
||||||
{
|
{
|
||||||
@ -119,7 +119,7 @@ public class OrganizationsController : Controller
|
|||||||
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
|
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushNotificationService = pushNotificationService;
|
||||||
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
||||||
_getSubscriptionQuery = getSubscriptionQuery;
|
_subscriberQueries = subscriberQueries;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
||||||
}
|
}
|
||||||
@ -479,7 +479,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await _getSubscriptionQuery.GetSubscription(organization);
|
var subscription = await _subscriberQueries.GetSubscriptionOrThrow(organization);
|
||||||
|
|
||||||
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
||||||
new OffboardingSurveyResponse
|
new OffboardingSurveyResponse
|
||||||
|
@ -4,9 +4,6 @@ namespace Bit.Api.AdminConsole.Models.Request;
|
|||||||
|
|
||||||
public class OrganizationDomainRequestModel
|
public class OrganizationDomainRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
|
||||||
public string Txt { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public string DomainName { get; set; }
|
public string DomainName { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -126,8 +126,14 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
|||||||
if (hideSensitiveData)
|
if (hideSensitiveData)
|
||||||
{
|
{
|
||||||
BillingEmail = null;
|
BillingEmail = null;
|
||||||
Subscription.Items = null;
|
if (Subscription != null)
|
||||||
UpcomingInvoice.Amount = null;
|
{
|
||||||
|
Subscription.Items = null;
|
||||||
|
}
|
||||||
|
if (UpcomingInvoice != null)
|
||||||
|
{
|
||||||
|
UpcomingInvoice.Amount = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ public class AccountsController : Controller
|
|||||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
||||||
private readonly IGetSubscriptionQuery _getSubscriptionQuery;
|
private readonly ISubscriberQueries _subscriberQueries;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ public class AccountsController : Controller
|
|||||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||||
IGetSubscriptionQuery getSubscriptionQuery,
|
ISubscriberQueries subscriberQueries,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||||
@ -133,7 +133,7 @@ public class AccountsController : Controller
|
|||||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
||||||
_getSubscriptionQuery = getSubscriptionQuery;
|
_subscriberQueries = subscriberQueries;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_cipherValidator = cipherValidator;
|
_cipherValidator = cipherValidator;
|
||||||
@ -831,7 +831,7 @@ public class AccountsController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await _getSubscriptionQuery.GetSubscription(user);
|
var subscription = await _subscriberQueries.GetSubscriptionOrThrow(user);
|
||||||
|
|
||||||
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
||||||
new OffboardingSurveyResponse
|
new OffboardingSurveyResponse
|
||||||
|
44
src/Api/Billing/Controllers/ProviderBillingController.cs
Normal file
44
src/Api/Billing/Controllers/ProviderBillingController.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using Bit.Api.Billing.Models;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Billing.Queries;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Controllers;
|
||||||
|
|
||||||
|
[Route("providers/{providerId:guid}/billing")]
|
||||||
|
[Authorize("Application")]
|
||||||
|
public class ProviderBillingController(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IProviderBillingQueries providerBillingQueries) : Controller
|
||||||
|
{
|
||||||
|
[HttpGet("subscription")]
|
||||||
|
public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)
|
||||||
|
{
|
||||||
|
if (!featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||||
|
{
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentContext.ProviderProviderAdmin(providerId))
|
||||||
|
{
|
||||||
|
return TypedResults.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriptionData = await providerBillingQueries.GetSubscriptionData(providerId);
|
||||||
|
|
||||||
|
if (subscriptionData == null)
|
||||||
|
{
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (providerPlans, subscription) = subscriptionData;
|
||||||
|
|
||||||
|
var providerSubscriptionDTO = ProviderSubscriptionDTO.From(providerPlans, subscription);
|
||||||
|
|
||||||
|
return TypedResults.Ok(providerSubscriptionDTO);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
using Bit.Api.Billing.Models;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Controllers;
|
||||||
|
|
||||||
|
[Route("providers/{providerId:guid}/organizations")]
|
||||||
|
public class ProviderOrganizationController(
|
||||||
|
IAssignSeatsToClientOrganizationCommand assignSeatsToClientOrganizationCommand,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IFeatureService featureService,
|
||||||
|
ILogger<ProviderOrganizationController> logger,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IProviderRepository providerRepository,
|
||||||
|
IProviderOrganizationRepository providerOrganizationRepository) : Controller
|
||||||
|
{
|
||||||
|
[HttpPut("{providerOrganizationId:guid}")]
|
||||||
|
public async Task<IResult> UpdateAsync(
|
||||||
|
[FromRoute] Guid providerId,
|
||||||
|
[FromRoute] Guid providerOrganizationId,
|
||||||
|
[FromBody] UpdateProviderOrganizationRequestBody requestBody)
|
||||||
|
{
|
||||||
|
if (!featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||||
|
{
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentContext.ProviderProviderAdmin(providerId))
|
||||||
|
{
|
||||||
|
return TypedResults.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider = await providerRepository.GetByIdAsync(providerId);
|
||||||
|
|
||||||
|
var providerOrganization = await providerOrganizationRepository.GetByIdAsync(providerOrganizationId);
|
||||||
|
|
||||||
|
if (provider == null || providerOrganization == null)
|
||||||
|
{
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||||
|
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
logger.LogError("The organization ({OrganizationID}) represented by provider organization ({ProviderOrganizationID}) could not be found.", providerOrganization.OrganizationId, providerOrganization.Id);
|
||||||
|
|
||||||
|
return TypedResults.Problem();
|
||||||
|
}
|
||||||
|
|
||||||
|
await assignSeatsToClientOrganizationCommand.AssignSeatsToClientOrganization(
|
||||||
|
provider,
|
||||||
|
organization,
|
||||||
|
requestBody.AssignedSeats);
|
||||||
|
|
||||||
|
return TypedResults.Ok();
|
||||||
|
}
|
||||||
|
}
|
49
src/Api/Billing/Models/ProviderSubscriptionDTO.cs
Normal file
49
src/Api/Billing/Models/ProviderSubscriptionDTO.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Models;
|
||||||
|
|
||||||
|
public record ProviderSubscriptionDTO(
|
||||||
|
string Status,
|
||||||
|
DateTime CurrentPeriodEndDate,
|
||||||
|
decimal? DiscountPercentage,
|
||||||
|
IEnumerable<ProviderPlanDTO> Plans)
|
||||||
|
{
|
||||||
|
private const string _annualCadence = "Annual";
|
||||||
|
private const string _monthlyCadence = "Monthly";
|
||||||
|
|
||||||
|
public static ProviderSubscriptionDTO From(
|
||||||
|
IEnumerable<ConfiguredProviderPlan> providerPlans,
|
||||||
|
Subscription subscription)
|
||||||
|
{
|
||||||
|
var providerPlansDTO = providerPlans
|
||||||
|
.Select(providerPlan =>
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||||
|
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.SeatPrice;
|
||||||
|
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
||||||
|
return new ProviderPlanDTO(
|
||||||
|
plan.Name,
|
||||||
|
providerPlan.SeatMinimum,
|
||||||
|
providerPlan.PurchasedSeats,
|
||||||
|
providerPlan.AssignedSeats,
|
||||||
|
cost,
|
||||||
|
cadence);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ProviderSubscriptionDTO(
|
||||||
|
subscription.Status,
|
||||||
|
subscription.CurrentPeriodEnd,
|
||||||
|
subscription.Customer?.Discount?.Coupon?.PercentOff,
|
||||||
|
providerPlansDTO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ProviderPlanDTO(
|
||||||
|
string PlanName,
|
||||||
|
int SeatMinimum,
|
||||||
|
int PurchasedSeats,
|
||||||
|
int AssignedSeats,
|
||||||
|
decimal Cost,
|
||||||
|
string Cadence);
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Api.Billing.Models;
|
||||||
|
|
||||||
|
public class UpdateProviderOrganizationRequestBody
|
||||||
|
{
|
||||||
|
public int AssignedSeats { get; set; }
|
||||||
|
}
|
@ -2,7 +2,6 @@
|
|||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -11,7 +10,6 @@ using Bit.Core.Models.Data;
|
|||||||
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -322,7 +320,6 @@ public class CollectionsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("bulk-access")]
|
[HttpPost("bulk-access")]
|
||||||
[RequireFeature(FeatureFlagKeys.BulkCollectionAccess)]
|
|
||||||
public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)
|
public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)
|
||||||
{
|
{
|
||||||
// Authorization logic assumes flexible collections is enabled
|
// Authorization logic assumes flexible collections is enabled
|
||||||
|
@ -42,11 +42,11 @@ public class PushController : Controller
|
|||||||
Prefix(model.UserId), Prefix(model.Identifier), model.Type);
|
Prefix(model.UserId), Prefix(model.Identifier), model.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpPost("delete")]
|
||||||
public async Task Delete(string id)
|
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
|
||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(id));
|
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("add-organization")]
|
[HttpPut("add-organization")]
|
||||||
@ -54,7 +54,8 @@ public class PushController : Controller
|
|||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
|
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
|
||||||
model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId));
|
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
|
||||||
|
Prefix(model.OrganizationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("delete-organization")]
|
[HttpPut("delete-organization")]
|
||||||
@ -62,7 +63,8 @@ public class PushController : Controller
|
|||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
|
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
|
||||||
model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId));
|
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
|
||||||
|
Prefix(model.OrganizationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("send")]
|
[HttpPost("send")]
|
||||||
|
@ -75,6 +75,10 @@ public class BillingSubscription
|
|||||||
{
|
{
|
||||||
Items = sub.Items.Select(i => new BillingSubscriptionItem(i));
|
Items = sub.Items.Select(i => new BillingSubscriptionItem(i));
|
||||||
}
|
}
|
||||||
|
CollectionMethod = sub.CollectionMethod;
|
||||||
|
SuspensionDate = sub.SuspensionDate;
|
||||||
|
UnpaidPeriodEndDate = sub.UnpaidPeriodEndDate;
|
||||||
|
GracePeriod = sub.GracePeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime? TrialStartDate { get; set; }
|
public DateTime? TrialStartDate { get; set; }
|
||||||
@ -86,6 +90,10 @@ public class BillingSubscription
|
|||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
public bool Cancelled { get; set; }
|
public bool Cancelled { get; set; }
|
||||||
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
||||||
|
public string CollectionMethod { get; set; }
|
||||||
|
public DateTime? SuspensionDate { get; set; }
|
||||||
|
public DateTime? UnpaidPeriodEndDate { get; set; }
|
||||||
|
public int? GracePeriod { get; set; }
|
||||||
|
|
||||||
public class BillingSubscriptionItem
|
public class BillingSubscriptionItem
|
||||||
{
|
{
|
||||||
|
@ -85,7 +85,7 @@ public class PeopleAccessPoliciesRequestModel
|
|||||||
|
|
||||||
if (!policies.All(ap => ap.Read && ap.Write))
|
if (!policies.All(ap => ap.Read && ap.Write))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Service account access must be Can read, write");
|
throw new BadRequestException("Machine account access must be Can read, write");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ServiceAccountPeopleAccessPolicies
|
return new ServiceAccountPeopleAccessPolicies
|
||||||
|
@ -170,8 +170,7 @@ public class Startup
|
|||||||
services.AddDefaultServices(globalSettings);
|
services.AddDefaultServices(globalSettings);
|
||||||
services.AddOrganizationSubscriptionServices();
|
services.AddOrganizationSubscriptionServices();
|
||||||
services.AddCoreLocalizationServices();
|
services.AddCoreLocalizationServices();
|
||||||
services.AddBillingCommands();
|
services.AddBillingOperations();
|
||||||
services.AddBillingQueries();
|
|
||||||
|
|
||||||
// Authorization Handlers
|
// Authorization Handlers
|
||||||
services.AddAuthorizationHandlers();
|
services.AddAuthorizationHandlers();
|
||||||
|
@ -560,7 +560,7 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
[HttpPut("{id}/collections")]
|
[HttpPut("{id}/collections")]
|
||||||
[HttpPost("{id}/collections")]
|
[HttpPost("{id}/collections")]
|
||||||
public async Task PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
public async Task<CipherResponseModel> PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||||
{
|
{
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var cipher = await GetByIdAsync(id, userId);
|
var cipher = await GetByIdAsync(id, userId);
|
||||||
@ -572,6 +572,10 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
await _cipherService.SaveCollectionsAsync(cipher,
|
await _cipherService.SaveCollectionsAsync(cipher,
|
||||||
model.CollectionIds.Select(c => new Guid(c)), userId, false);
|
model.CollectionIds.Select(c => new Guid(c)), userId, false);
|
||||||
|
|
||||||
|
var updatedCipherCollections = await GetByIdAsync(id, userId);
|
||||||
|
var response = new CipherResponseModel(updatedCipherCollections, _globalSettings);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}/collections-admin")]
|
[HttpPut("{id}/collections-admin")]
|
||||||
@ -1106,6 +1110,33 @@ public class CiphersController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the user is an admin or owner of an organization with unassigned ciphers (i.e. ciphers that
|
||||||
|
/// are not assigned to a collection).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("has-unassigned-ciphers")]
|
||||||
|
public async Task<bool> HasUnassignedCiphers()
|
||||||
|
{
|
||||||
|
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||||
|
|
||||||
|
var adminOrganizations = _currentContext.Organizations
|
||||||
|
.Where(o => o.Type is OrganizationUserType.Admin or OrganizationUserType.Owner &&
|
||||||
|
orgAbilities.ContainsKey(o.Id) && orgAbilities[o.Id].FlexibleCollections);
|
||||||
|
|
||||||
|
foreach (var org in adminOrganizations)
|
||||||
|
{
|
||||||
|
var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(org.Id);
|
||||||
|
// We only care about non-deleted ciphers
|
||||||
|
if (unassignedCiphers.Any(c => c.DeletedDate == null))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void ValidateAttachment()
|
private void ValidateAttachment()
|
||||||
{
|
{
|
||||||
if (!Request?.ContentType.Contains("multipart/") ?? true)
|
if (!Request?.ContentType.Contains("multipart/") ?? true)
|
||||||
|
@ -32,10 +32,6 @@
|
|||||||
"send": {
|
"send": {
|
||||||
"connectionString": "SECRET"
|
"connectionString": "SECRET"
|
||||||
},
|
},
|
||||||
"documentDb": {
|
|
||||||
"uri": "SECRET",
|
|
||||||
"key": "SECRET"
|
|
||||||
},
|
|
||||||
"sentry": {
|
"sentry": {
|
||||||
"dsn": "SECRET"
|
"dsn": "SECRET"
|
||||||
},
|
},
|
||||||
|
@ -75,7 +75,7 @@ public class PayPalController : Controller
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(transactionModel.TransactionId))
|
if (string.IsNullOrEmpty(transactionModel.TransactionId))
|
||||||
{
|
{
|
||||||
_logger.LogError("PayPal IPN: Transaction ID is missing");
|
_logger.LogWarning("PayPal IPN: Transaction ID is missing");
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,7 +453,7 @@ public class StripeController : Controller
|
|||||||
}
|
}
|
||||||
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeRefunded))
|
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeRefunded))
|
||||||
{
|
{
|
||||||
var charge = await _stripeEventService.GetCharge(parsedEvent);
|
var charge = await _stripeEventService.GetCharge(parsedEvent, true, ["refunds"]);
|
||||||
var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||||
GatewayType.Stripe, charge.Id);
|
GatewayType.Stripe, charge.Id);
|
||||||
if (chargeTransaction == null)
|
if (chargeTransaction == null)
|
||||||
@ -868,7 +868,7 @@ public class StripeController : Controller
|
|||||||
private bool UnpaidAutoChargeInvoiceForSubscriptionCycle(Invoice invoice)
|
private bool UnpaidAutoChargeInvoiceForSubscriptionCycle(Invoice invoice)
|
||||||
{
|
{
|
||||||
return invoice.AmountDue > 0 && !invoice.Paid && invoice.CollectionMethod == "charge_automatically" &&
|
return invoice.AmountDue > 0 && !invoice.Paid && invoice.CollectionMethod == "charge_automatically" &&
|
||||||
invoice.BillingReason == "subscription_cycle" && invoice.SubscriptionId != null;
|
invoice.BillingReason is "subscription_cycle" or "automatic_pending_invoice_item_invoice" && invoice.SubscriptionId != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Subscription> VerifyCorrectTaxRateForCharge(Invoice invoice, Subscription subscription)
|
private async Task<Subscription> VerifyCorrectTaxRateForCharge(Invoice invoice, Subscription subscription)
|
||||||
|
@ -30,10 +30,6 @@
|
|||||||
"connectionString": "SECRET",
|
"connectionString": "SECRET",
|
||||||
"applicationCacheTopicName": "SECRET"
|
"applicationCacheTopicName": "SECRET"
|
||||||
},
|
},
|
||||||
"documentDb": {
|
|
||||||
"uri": "SECRET",
|
|
||||||
"key": "SECRET"
|
|
||||||
},
|
|
||||||
"sentry": {
|
"sentry": {
|
||||||
"dsn": "SECRET"
|
"dsn": "SECRET"
|
||||||
},
|
},
|
||||||
|
@ -6,7 +6,7 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Entities.Provider;
|
namespace Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
|
||||||
public class Provider : ITableObject<Guid>
|
public class Provider : ITableObject<Guid>, ISubscriber
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -34,6 +34,26 @@ public class Provider : ITableObject<Guid>
|
|||||||
public string GatewayCustomerId { get; set; }
|
public string GatewayCustomerId { get; set; }
|
||||||
public string GatewaySubscriptionId { get; set; }
|
public string GatewaySubscriptionId { get; set; }
|
||||||
|
|
||||||
|
public string BillingEmailAddress() => BillingEmail?.ToLowerInvariant().Trim();
|
||||||
|
|
||||||
|
public string BillingName() => DisplayBusinessName();
|
||||||
|
|
||||||
|
public string SubscriberName() => DisplayName();
|
||||||
|
|
||||||
|
public string BraintreeCustomerIdPrefix() => "p";
|
||||||
|
|
||||||
|
public string BraintreeIdField() => "provider_id";
|
||||||
|
|
||||||
|
public string BraintreeCloudRegionField() => "region";
|
||||||
|
|
||||||
|
public bool IsOrganization() => false;
|
||||||
|
|
||||||
|
public bool IsUser() => false;
|
||||||
|
|
||||||
|
public string SubscriberType() => "Provider";
|
||||||
|
|
||||||
|
public bool IsExpired() => false;
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
if (Id == default)
|
if (Id == default)
|
||||||
|
@ -146,7 +146,8 @@ public class SelfHostedOrganizationDetails : Organization
|
|||||||
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
||||||
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
||||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
||||||
FlexibleCollections = FlexibleCollections
|
FlexibleCollections = FlexibleCollections,
|
||||||
|
Status = Status
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||||
@ -50,26 +51,16 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
|
|||||||
throw new ConflictException("A domain already exists for this organization.");
|
throw new ConflictException("A domain already exists for this organization.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
// Generate and set DNS TXT Record
|
||||||
{
|
// DNS-Based Service Discovery RFC: https://www.ietf.org/rfc/rfc6763.txt; see section 6.1
|
||||||
if (await _dnsResolverService.ResolveAsync(organizationDomain.DomainName, organizationDomain.Txt))
|
// Google uses 43 chars for their TXT record value: https://support.google.com/a/answer/2716802
|
||||||
{
|
// A random 44 character string was used here to keep parity with prior client-side generation of 47 characters
|
||||||
organizationDomain.SetVerifiedDate();
|
organizationDomain.Txt = string.Join("=", "bw", CoreHelpers.RandomString(44));
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, "Error verifying Organization domain.");
|
|
||||||
}
|
|
||||||
|
|
||||||
organizationDomain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
organizationDomain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||||
organizationDomain.SetLastCheckedDate();
|
|
||||||
|
|
||||||
var orgDomain = await _organizationDomainRepository.CreateAsync(organizationDomain);
|
var orgDomain = await _organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
await _eventService.LogOrganizationDomainEventAsync(orgDomain, EventType.OrganizationDomain_Added);
|
await _eventService.LogOrganizationDomainEventAsync(orgDomain, EventType.OrganizationDomain_Added);
|
||||||
await _eventService.LogOrganizationDomainEventAsync(orgDomain,
|
|
||||||
orgDomain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
|
|
||||||
|
|
||||||
return orgDomain;
|
return orgDomain;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,6 @@ namespace Bit.Core.AdminConsole.Providers.Interfaces;
|
|||||||
|
|
||||||
public interface ICreateProviderCommand
|
public interface ICreateProviderCommand
|
||||||
{
|
{
|
||||||
Task CreateMspAsync(Provider provider, string ownerEmail);
|
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
|
||||||
Task CreateResellerAsync(Provider provider);
|
Task CreateResellerAsync(Provider provider);
|
||||||
}
|
}
|
||||||
|
@ -681,8 +681,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
await _organizationUserRepository.CreateAsync(orgUser);
|
await _organizationUserRepository.CreateAsync(orgUser);
|
||||||
|
|
||||||
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
||||||
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds,
|
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices,
|
||||||
organization.Id.ToString());
|
organization.Id.ToString());
|
||||||
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
|
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
|
||||||
}
|
}
|
||||||
@ -1932,17 +1932,19 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
||||||
{
|
{
|
||||||
var deviceIds = await GetUserDeviceIdsAsync(userId);
|
var devices = await GetUserDeviceIdsAsync(userId);
|
||||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds,
|
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
|
||||||
organizationId.ToString());
|
organizationId.ToString());
|
||||||
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||||
return devices.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)).Select(d => d.Id.ToString());
|
return devices
|
||||||
|
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||||
|
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
||||||
@ -2037,7 +2039,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
|
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan does not allow additional Service Accounts.");
|
throw new BadRequestException("Plan does not allow additional Machine Accounts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((plan.Product == ProductType.TeamsStarter &&
|
if ((plan.Product == ProductType.TeamsStarter &&
|
||||||
@ -2050,7 +2052,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)
|
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You can't subtract Service Accounts!");
|
throw new BadRequestException("You can't subtract Machine Accounts!");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (plan.SecretsManager.HasAdditionalSeatsOption)
|
switch (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||||
|
9
src/Core/Billing/BillingException.cs
Normal file
9
src/Core/Billing/BillingException.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Bit.Core.Billing;
|
||||||
|
|
||||||
|
public class BillingException(
|
||||||
|
string clientFriendlyMessage,
|
||||||
|
string internalMessage = null,
|
||||||
|
Exception innerException = null) : Exception(internalMessage, innerException)
|
||||||
|
{
|
||||||
|
public string ClientFriendlyMessage { get; set; } = clientFriendlyMessage;
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Commands;
|
||||||
|
|
||||||
|
public interface IAssignSeatsToClientOrganizationCommand
|
||||||
|
{
|
||||||
|
Task AssignSeatsToClientOrganization(
|
||||||
|
Provider provider,
|
||||||
|
Organization organization,
|
||||||
|
int seats);
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Commands;
|
namespace Bit.Core.Billing.Commands;
|
||||||
@ -17,7 +16,6 @@ public interface ICancelSubscriptionCommand
|
|||||||
/// <param name="subscription">The <see cref="User"/> or <see cref="Organization"/> with the subscription to cancel.</param>
|
/// <param name="subscription">The <see cref="User"/> or <see cref="Organization"/> with the subscription to cancel.</param>
|
||||||
/// <param name="offboardingSurveyResponse">An <see cref="OffboardingSurveyResponse"/> DTO containing user-provided feedback on why they are cancelling the subscription.</param>
|
/// <param name="offboardingSurveyResponse">An <see cref="OffboardingSurveyResponse"/> DTO containing user-provided feedback on why they are cancelling the subscription.</param>
|
||||||
/// <param name="cancelImmediately">A flag indicating whether to cancel the subscription immediately or at the end of the subscription period.</param>
|
/// <param name="cancelImmediately">A flag indicating whether to cancel the subscription immediately or at the end of the subscription period.</param>
|
||||||
/// <exception cref="GatewayException">Thrown when the provided subscription is already in an inactive state.</exception>
|
|
||||||
Task CancelSubscription(
|
Task CancelSubscription(
|
||||||
Subscription subscription,
|
Subscription subscription,
|
||||||
OffboardingSurveyResponse offboardingSurveyResponse,
|
OffboardingSurveyResponse offboardingSurveyResponse,
|
||||||
|
@ -4,5 +4,12 @@ namespace Bit.Core.Billing.Commands;
|
|||||||
|
|
||||||
public interface IRemovePaymentMethodCommand
|
public interface IRemovePaymentMethodCommand
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to remove an Organization's saved payment method. If the Stripe <see cref="Stripe.Customer"/> representing the
|
||||||
|
/// <see cref="Organization"/> contains a valid <b>"btCustomerId"</b> key in its <see cref="Stripe.Customer.Metadata"/> property,
|
||||||
|
/// this command will attempt to remove the Braintree <see cref="Braintree.PaymentMethod"/>. Otherwise, it will attempt to remove the
|
||||||
|
/// Stripe <see cref="Stripe.PaymentMethod"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organization">The organization to remove the saved payment method for.</param>
|
||||||
Task RemovePaymentMethod(Organization organization);
|
Task RemovePaymentMethod(Organization organization);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
|
using Bit.Core.Billing.Queries;
|
||||||
|
using Bit.Core.Billing.Repositories;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Bit.Core.Billing.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Commands.Implementations;
|
||||||
|
|
||||||
|
public class AssignSeatsToClientOrganizationCommand(
|
||||||
|
ILogger<AssignSeatsToClientOrganizationCommand> logger,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IPaymentService paymentService,
|
||||||
|
IProviderBillingQueries providerBillingQueries,
|
||||||
|
IProviderPlanRepository providerPlanRepository) : IAssignSeatsToClientOrganizationCommand
|
||||||
|
{
|
||||||
|
public async Task AssignSeatsToClientOrganization(
|
||||||
|
Provider provider,
|
||||||
|
Organization organization,
|
||||||
|
int seats)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(provider);
|
||||||
|
ArgumentNullException.ThrowIfNull(organization);
|
||||||
|
|
||||||
|
if (provider.Type == ProviderType.Reseller)
|
||||||
|
{
|
||||||
|
logger.LogError("Reseller-type provider ({ID}) cannot assign seats to client organizations", provider.Id);
|
||||||
|
|
||||||
|
throw ContactSupport("Consolidated billing does not support reseller-type providers");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seats < 0)
|
||||||
|
{
|
||||||
|
throw new BillingException(
|
||||||
|
"You cannot assign negative seats to a client.",
|
||||||
|
"MSP cannot assign negative seats to a client organization");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seats == organization.Seats)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Client organization ({ID}) already has {Seats} seats assigned", organization.Id, organization.Seats);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerPlan = await GetProviderPlanAsync(provider, organization);
|
||||||
|
|
||||||
|
var providerSeatMinimum = providerPlan.SeatMinimum.GetValueOrDefault(0);
|
||||||
|
|
||||||
|
// How many seats the provider has assigned to all their client organizations that have the specified plan type.
|
||||||
|
var providerCurrentlyAssignedSeatTotal = await providerBillingQueries.GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType);
|
||||||
|
|
||||||
|
// How many seats are being added to or subtracted from this client organization.
|
||||||
|
var seatDifference = seats - (organization.Seats ?? 0);
|
||||||
|
|
||||||
|
// How many seats the provider will have assigned to all of their client organizations after the update.
|
||||||
|
var providerNewlyAssignedSeatTotal = providerCurrentlyAssignedSeatTotal + seatDifference;
|
||||||
|
|
||||||
|
var update = CurryUpdateFunction(
|
||||||
|
provider,
|
||||||
|
providerPlan,
|
||||||
|
organization,
|
||||||
|
seats,
|
||||||
|
providerNewlyAssignedSeatTotal);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Below the limit => Below the limit:
|
||||||
|
* No subscription update required. We can safely update the organization's seats.
|
||||||
|
*/
|
||||||
|
if (providerCurrentlyAssignedSeatTotal <= providerSeatMinimum &&
|
||||||
|
providerNewlyAssignedSeatTotal <= providerSeatMinimum)
|
||||||
|
{
|
||||||
|
organization.Seats = seats;
|
||||||
|
|
||||||
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
|
providerPlan.AllocatedSeats = providerNewlyAssignedSeatTotal;
|
||||||
|
|
||||||
|
await providerPlanRepository.ReplaceAsync(providerPlan);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Below the limit => Above the limit:
|
||||||
|
* We have to scale the subscription up from the seat minimum to the newly assigned seat total.
|
||||||
|
*/
|
||||||
|
else if (providerCurrentlyAssignedSeatTotal <= providerSeatMinimum &&
|
||||||
|
providerNewlyAssignedSeatTotal > providerSeatMinimum)
|
||||||
|
{
|
||||||
|
await update(
|
||||||
|
providerSeatMinimum,
|
||||||
|
providerNewlyAssignedSeatTotal);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Above the limit => Above the limit:
|
||||||
|
* We have to scale the subscription from the currently assigned seat total to the newly assigned seat total.
|
||||||
|
*/
|
||||||
|
else if (providerCurrentlyAssignedSeatTotal > providerSeatMinimum &&
|
||||||
|
providerNewlyAssignedSeatTotal > providerSeatMinimum)
|
||||||
|
{
|
||||||
|
await update(
|
||||||
|
providerCurrentlyAssignedSeatTotal,
|
||||||
|
providerNewlyAssignedSeatTotal);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Above the limit => Below the limit:
|
||||||
|
* We have to scale the subscription down from the currently assigned seat total to the seat minimum.
|
||||||
|
*/
|
||||||
|
else if (providerCurrentlyAssignedSeatTotal > providerSeatMinimum &&
|
||||||
|
providerNewlyAssignedSeatTotal <= providerSeatMinimum)
|
||||||
|
{
|
||||||
|
await update(
|
||||||
|
providerCurrentlyAssignedSeatTotal,
|
||||||
|
providerSeatMinimum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
|
private async Task<ProviderPlan> GetProviderPlanAsync(Provider provider, Organization organization)
|
||||||
|
{
|
||||||
|
if (!organization.PlanType.SupportsConsolidatedBilling())
|
||||||
|
{
|
||||||
|
logger.LogError("Cannot assign seats to a client organization ({ID}) with a plan type that does not support consolidated billing: {PlanType}", organization.Id, organization.PlanType);
|
||||||
|
|
||||||
|
throw ContactSupport();
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||||
|
|
||||||
|
var providerPlan = providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == organization.PlanType);
|
||||||
|
|
||||||
|
if (providerPlan != null && providerPlan.IsConfigured())
|
||||||
|
{
|
||||||
|
return providerPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogError("Cannot assign seats to client organization ({ClientOrganizationID}) when provider's ({ProviderID}) matching plan is not configured", organization.Id, provider.Id);
|
||||||
|
|
||||||
|
throw ContactSupport();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Func<int, int, Task> CurryUpdateFunction(
|
||||||
|
Provider provider,
|
||||||
|
ProviderPlan providerPlan,
|
||||||
|
Organization organization,
|
||||||
|
int organizationNewlyAssignedSeats,
|
||||||
|
int providerNewlyAssignedSeats) => async (providerCurrentlySubscribedSeats, providerNewlySubscribedSeats) =>
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||||
|
|
||||||
|
await paymentService.AdjustSeats(
|
||||||
|
provider,
|
||||||
|
plan,
|
||||||
|
providerCurrentlySubscribedSeats,
|
||||||
|
providerNewlySubscribedSeats);
|
||||||
|
|
||||||
|
organization.Seats = organizationNewlyAssignedSeats;
|
||||||
|
|
||||||
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
|
var providerNewlyPurchasedSeats = providerNewlySubscribedSeats > providerPlan.SeatMinimum
|
||||||
|
? providerNewlySubscribedSeats - providerPlan.SeatMinimum
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
providerPlan.PurchasedSeats = providerNewlyPurchasedSeats;
|
||||||
|
providerPlan.AllocatedSeats = providerNewlyAssignedSeats;
|
||||||
|
|
||||||
|
await providerPlanRepository.ReplaceAsync(providerPlan);
|
||||||
|
};
|
||||||
|
}
|
@ -1,55 +1,41 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Braintree;
|
using Braintree;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using static Bit.Core.Billing.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Commands.Implementations;
|
namespace Bit.Core.Billing.Commands.Implementations;
|
||||||
|
|
||||||
public class RemovePaymentMethodCommand : IRemovePaymentMethodCommand
|
public class RemovePaymentMethodCommand(
|
||||||
|
IBraintreeGateway braintreeGateway,
|
||||||
|
ILogger<RemovePaymentMethodCommand> logger,
|
||||||
|
IStripeAdapter stripeAdapter)
|
||||||
|
: IRemovePaymentMethodCommand
|
||||||
{
|
{
|
||||||
private readonly IBraintreeGateway _braintreeGateway;
|
|
||||||
private readonly ILogger<RemovePaymentMethodCommand> _logger;
|
|
||||||
private readonly IStripeAdapter _stripeAdapter;
|
|
||||||
|
|
||||||
public RemovePaymentMethodCommand(
|
|
||||||
IBraintreeGateway braintreeGateway,
|
|
||||||
ILogger<RemovePaymentMethodCommand> logger,
|
|
||||||
IStripeAdapter stripeAdapter)
|
|
||||||
{
|
|
||||||
_braintreeGateway = braintreeGateway;
|
|
||||||
_logger = logger;
|
|
||||||
_stripeAdapter = stripeAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemovePaymentMethod(Organization organization)
|
public async Task RemovePaymentMethod(Organization organization)
|
||||||
{
|
{
|
||||||
const string braintreeCustomerIdKey = "btCustomerId";
|
ArgumentNullException.ThrowIfNull(organization);
|
||||||
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(organization));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization.Gateway is not GatewayType.Stripe || string.IsNullOrEmpty(organization.GatewayCustomerId))
|
if (organization.Gateway is not GatewayType.Stripe || string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
throw ContactSupport();
|
throw ContactSupport();
|
||||||
}
|
}
|
||||||
|
|
||||||
var stripeCustomer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId, new Stripe.CustomerGetOptions
|
var stripeCustomer = await stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId, new Stripe.CustomerGetOptions
|
||||||
{
|
{
|
||||||
Expand = new List<string> { "invoice_settings.default_payment_method", "sources" }
|
Expand = ["invoice_settings.default_payment_method", "sources"]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stripeCustomer == null)
|
if (stripeCustomer == null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Could not find Stripe customer ({ID}) when removing payment method", organization.GatewayCustomerId);
|
logger.LogError("Could not find Stripe customer ({ID}) when removing payment method", organization.GatewayCustomerId);
|
||||||
|
|
||||||
throw ContactSupport();
|
throw ContactSupport();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stripeCustomer.Metadata?.TryGetValue(braintreeCustomerIdKey, out var braintreeCustomerId) ?? false)
|
if (stripeCustomer.Metadata?.TryGetValue(BraintreeCustomerIdKey, out var braintreeCustomerId) ?? false)
|
||||||
{
|
{
|
||||||
await RemoveBraintreePaymentMethodAsync(braintreeCustomerId);
|
await RemoveBraintreePaymentMethodAsync(braintreeCustomerId);
|
||||||
}
|
}
|
||||||
@ -61,11 +47,11 @@ public class RemovePaymentMethodCommand : IRemovePaymentMethodCommand
|
|||||||
|
|
||||||
private async Task RemoveBraintreePaymentMethodAsync(string braintreeCustomerId)
|
private async Task RemoveBraintreePaymentMethodAsync(string braintreeCustomerId)
|
||||||
{
|
{
|
||||||
var customer = await _braintreeGateway.Customer.FindAsync(braintreeCustomerId);
|
var customer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId);
|
||||||
|
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to retrieve Braintree customer ({ID}) when removing payment method", braintreeCustomerId);
|
logger.LogError("Failed to retrieve Braintree customer ({ID}) when removing payment method", braintreeCustomerId);
|
||||||
|
|
||||||
throw ContactSupport();
|
throw ContactSupport();
|
||||||
}
|
}
|
||||||
@ -74,27 +60,27 @@ public class RemovePaymentMethodCommand : IRemovePaymentMethodCommand
|
|||||||
{
|
{
|
||||||
var existingDefaultPaymentMethod = customer.DefaultPaymentMethod;
|
var existingDefaultPaymentMethod = customer.DefaultPaymentMethod;
|
||||||
|
|
||||||
var updateCustomerResult = await _braintreeGateway.Customer.UpdateAsync(
|
var updateCustomerResult = await braintreeGateway.Customer.UpdateAsync(
|
||||||
braintreeCustomerId,
|
braintreeCustomerId,
|
||||||
new CustomerRequest { DefaultPaymentMethodToken = null });
|
new CustomerRequest { DefaultPaymentMethodToken = null });
|
||||||
|
|
||||||
if (!updateCustomerResult.IsSuccess())
|
if (!updateCustomerResult.IsSuccess())
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to update payment method for Braintree customer ({ID}) | Message: {Message}",
|
logger.LogError("Failed to update payment method for Braintree customer ({ID}) | Message: {Message}",
|
||||||
braintreeCustomerId, updateCustomerResult.Message);
|
braintreeCustomerId, updateCustomerResult.Message);
|
||||||
|
|
||||||
throw ContactSupport();
|
throw ContactSupport();
|
||||||
}
|
}
|
||||||
|
|
||||||
var deletePaymentMethodResult = await _braintreeGateway.PaymentMethod.DeleteAsync(existingDefaultPaymentMethod.Token);
|
var deletePaymentMethodResult = await braintreeGateway.PaymentMethod.DeleteAsync(existingDefaultPaymentMethod.Token);
|
||||||
|
|
||||||
if (!deletePaymentMethodResult.IsSuccess())
|
if (!deletePaymentMethodResult.IsSuccess())
|
||||||
{
|
{
|
||||||
await _braintreeGateway.Customer.UpdateAsync(
|
await braintreeGateway.Customer.UpdateAsync(
|
||||||
braintreeCustomerId,
|
braintreeCustomerId,
|
||||||
new CustomerRequest { DefaultPaymentMethodToken = existingDefaultPaymentMethod.Token });
|
new CustomerRequest { DefaultPaymentMethodToken = existingDefaultPaymentMethod.Token });
|
||||||
|
|
||||||
_logger.LogError(
|
logger.LogError(
|
||||||
"Failed to delete Braintree payment method for Customer ({ID}), re-linked payment method. Message: {Message}",
|
"Failed to delete Braintree payment method for Customer ({ID}), re-linked payment method. Message: {Message}",
|
||||||
braintreeCustomerId, deletePaymentMethodResult.Message);
|
braintreeCustomerId, deletePaymentMethodResult.Message);
|
||||||
|
|
||||||
@ -103,7 +89,7 @@ public class RemovePaymentMethodCommand : IRemovePaymentMethodCommand
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Tried to remove non-existent Braintree payment method for Customer ({ID})", braintreeCustomerId);
|
logger.LogWarning("Tried to remove non-existent Braintree payment method for Customer ({ID})", braintreeCustomerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,25 +102,23 @@ public class RemovePaymentMethodCommand : IRemovePaymentMethodCommand
|
|||||||
switch (source)
|
switch (source)
|
||||||
{
|
{
|
||||||
case Stripe.BankAccount:
|
case Stripe.BankAccount:
|
||||||
await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
|
await stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
|
||||||
break;
|
break;
|
||||||
case Stripe.Card:
|
case Stripe.Card:
|
||||||
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
|
await stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var paymentMethods = _stripeAdapter.PaymentMethodListAutoPagingAsync(new Stripe.PaymentMethodListOptions
|
var paymentMethods = stripeAdapter.PaymentMethodListAutoPagingAsync(new Stripe.PaymentMethodListOptions
|
||||||
{
|
{
|
||||||
Customer = customer.Id
|
Customer = customer.Id
|
||||||
});
|
});
|
||||||
|
|
||||||
await foreach (var paymentMethod in paymentMethods)
|
await foreach (var paymentMethod in paymentMethods)
|
||||||
{
|
{
|
||||||
await _stripeAdapter.PaymentMethodDetachAsync(paymentMethod.Id, new Stripe.PaymentMethodDetachOptions());
|
await stripeAdapter.PaymentMethodDetachAsync(paymentMethod.Id, new Stripe.PaymentMethodDetachOptions());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GatewayException ContactSupport() => new("Could not remove your payment method. Please contact support for assistance.");
|
|
||||||
}
|
}
|
||||||
|
@ -20,4 +20,6 @@ public class ProviderPlan : ITableObject<Guid>
|
|||||||
Id = CoreHelpers.GenerateComb();
|
Id = CoreHelpers.GenerateComb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsConfigured() => SeatMinimum.HasValue && PurchasedSeats.HasValue && AllocatedSeats.HasValue;
|
||||||
}
|
}
|
||||||
|
9
src/Core/Billing/Extensions/BillingExtensions.cs
Normal file
9
src/Core/Billing/Extensions/BillingExtensions.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Extensions;
|
||||||
|
|
||||||
|
public static class BillingExtensions
|
||||||
|
{
|
||||||
|
public static bool SupportsConsolidatedBilling(this PlanType planType)
|
||||||
|
=> planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly;
|
||||||
|
}
|
@ -9,14 +9,15 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static void AddBillingCommands(this IServiceCollection services)
|
public static void AddBillingOperations(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<ICancelSubscriptionCommand, CancelSubscriptionCommand>();
|
// Queries
|
||||||
services.AddSingleton<IRemovePaymentMethodCommand, RemovePaymentMethodCommand>();
|
services.AddTransient<IProviderBillingQueries, ProviderBillingQueries>();
|
||||||
}
|
services.AddTransient<ISubscriberQueries, SubscriberQueries>();
|
||||||
|
|
||||||
public static void AddBillingQueries(this IServiceCollection services)
|
// Commands
|
||||||
{
|
services.AddTransient<IAssignSeatsToClientOrganizationCommand, AssignSeatsToClientOrganizationCommand>();
|
||||||
services.AddSingleton<IGetSubscriptionQuery, GetSubscriptionQuery>();
|
services.AddTransient<ICancelSubscriptionCommand, CancelSubscriptionCommand>();
|
||||||
|
services.AddTransient<IRemovePaymentMethodCommand, RemovePaymentMethodCommand>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
src/Core/Billing/Models/ConfiguredProviderPlan.cs
Normal file
24
src/Core/Billing/Models/ConfiguredProviderPlan.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Models;
|
||||||
|
|
||||||
|
public record ConfiguredProviderPlan(
|
||||||
|
Guid Id,
|
||||||
|
Guid ProviderId,
|
||||||
|
PlanType PlanType,
|
||||||
|
int SeatMinimum,
|
||||||
|
int PurchasedSeats,
|
||||||
|
int AssignedSeats)
|
||||||
|
{
|
||||||
|
public static ConfiguredProviderPlan From(ProviderPlan providerPlan) =>
|
||||||
|
providerPlan.IsConfigured()
|
||||||
|
? new ConfiguredProviderPlan(
|
||||||
|
providerPlan.Id,
|
||||||
|
providerPlan.ProviderId,
|
||||||
|
providerPlan.PlanType,
|
||||||
|
providerPlan.SeatMinimum.GetValueOrDefault(0),
|
||||||
|
providerPlan.PurchasedSeats.GetValueOrDefault(0),
|
||||||
|
providerPlan.AllocatedSeats.GetValueOrDefault(0))
|
||||||
|
: null;
|
||||||
|
}
|
7
src/Core/Billing/Models/ProviderSubscriptionData.cs
Normal file
7
src/Core/Billing/Models/ProviderSubscriptionData.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Models;
|
||||||
|
|
||||||
|
public record ProviderSubscriptionData(
|
||||||
|
List<ConfiguredProviderPlan> ProviderPlans,
|
||||||
|
Subscription Subscription);
|
@ -1,18 +0,0 @@
|
|||||||
using Bit.Core.Entities;
|
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Stripe;
|
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Queries;
|
|
||||||
|
|
||||||
public interface IGetSubscriptionQuery
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a Stripe <see cref="Subscription"/> using the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewaySubscriptionId"/> property.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscriber">The organization or user to retrieve the subscription for.</param>
|
|
||||||
/// <returns>A Stripe <see cref="Subscription"/>.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="subscriber"/> is <see langword="null"/>.</exception>
|
|
||||||
/// <exception cref="GatewayException">Thrown when the subscriber's <see cref="ISubscriber.GatewaySubscriptionId"/> is <see langword="null"/> or empty.</exception>
|
|
||||||
/// <exception cref="GatewayException">Thrown when the <see cref="Subscription"/> returned from Stripe's API is null.</exception>
|
|
||||||
Task<Subscription> GetSubscription(ISubscriber subscriber);
|
|
||||||
}
|
|
27
src/Core/Billing/Queries/IProviderBillingQueries.cs
Normal file
27
src/Core/Billing/Queries/IProviderBillingQueries.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Queries;
|
||||||
|
|
||||||
|
public interface IProviderBillingQueries
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the number of seats an MSP has assigned to its client organizations with a specified <paramref name="planType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="providerId">The ID of the MSP to retrieve the assigned seat total for.</param>
|
||||||
|
/// <param name="planType">The type of plan to retrieve the assigned seat total for.</param>
|
||||||
|
/// <returns>An <see cref="int"/> representing the number of seats the provider has assigned to its client organizations with the specified <paramref name="planType"/>.</returns>
|
||||||
|
/// <exception cref="BillingException">Thrown when the provider represented by the <paramref name="providerId"/> is <see langword="null"/>.</exception>
|
||||||
|
/// <exception cref="BillingException">Thrown when the provider represented by the <paramref name="providerId"/> has <see cref="Provider.Type"/> <see cref="ProviderType.Reseller"/>.</exception>
|
||||||
|
Task<int> GetAssignedSeatTotalForPlanOrThrow(Guid providerId, PlanType planType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a provider's billing subscription data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="providerId">The ID of the provider to retrieve subscription data for.</param>
|
||||||
|
/// <returns>A <see cref="ProviderSubscriptionData"/> object containing the provider's Stripe <see cref="Stripe.Subscription"/> and their <see cref="ConfiguredProviderPlan"/>s.</returns>
|
||||||
|
/// <remarks>This method opts for returning <see langword="null"/> rather than throwing exceptions, making it ideal for surfacing data from API endpoints.</remarks>
|
||||||
|
Task<ProviderSubscriptionData> GetSubscriptionData(Guid providerId);
|
||||||
|
}
|
30
src/Core/Billing/Queries/ISubscriberQueries.cs
Normal file
30
src/Core/Billing/Queries/ISubscriberQueries.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Queries;
|
||||||
|
|
||||||
|
public interface ISubscriberQueries
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a Stripe <see cref="Subscription"/> using the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewaySubscriptionId"/> property.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscriber">The organization, provider or user to retrieve the subscription for.</param>
|
||||||
|
/// <param name="subscriptionGetOptions">Optional parameters that can be passed to Stripe to expand or modify the <see cref="Subscription"/>.</param>
|
||||||
|
/// <returns>A Stripe <see cref="Subscription"/>.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="subscriber"/> is <see langword="null"/>.</exception>
|
||||||
|
/// <remarks>This method opts for returning <see langword="null"/> rather than throwing exceptions, making it ideal for surfacing data from API endpoints.</remarks>
|
||||||
|
Task<Subscription> GetSubscription(
|
||||||
|
ISubscriber subscriber,
|
||||||
|
SubscriptionGetOptions subscriptionGetOptions = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a Stripe <see cref="Subscription"/> using the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewaySubscriptionId"/> property.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscriber">The organization or user to retrieve the subscription for.</param>
|
||||||
|
/// <returns>A Stripe <see cref="Subscription"/>.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="subscriber"/> is <see langword="null"/>.</exception>
|
||||||
|
/// <exception cref="GatewayException">Thrown when the subscriber's <see cref="ISubscriber.GatewaySubscriptionId"/> is <see langword="null"/> or empty.</exception>
|
||||||
|
/// <exception cref="GatewayException">Thrown when the <see cref="Subscription"/> returned from Stripe's API is null.</exception>
|
||||||
|
Task<Subscription> GetSubscriptionOrThrow(ISubscriber subscriber);
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
using Bit.Core.Entities;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Stripe;
|
|
||||||
|
|
||||||
using static Bit.Core.Billing.Utilities;
|
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Queries.Implementations;
|
|
||||||
|
|
||||||
public class GetSubscriptionQuery(
|
|
||||||
ILogger<GetSubscriptionQuery> logger,
|
|
||||||
IStripeAdapter stripeAdapter) : IGetSubscriptionQuery
|
|
||||||
{
|
|
||||||
public async Task<Subscription> GetSubscription(ISubscriber subscriber)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(subscriber);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
|
|
||||||
{
|
|
||||||
logger.LogError("Cannot cancel subscription for subscriber ({ID}) with no GatewaySubscriptionId.", subscriber.Id);
|
|
||||||
|
|
||||||
throw ContactSupport();
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscription = await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
|
|
||||||
|
|
||||||
if (subscription != null)
|
|
||||||
{
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogError("Could not find Stripe subscription ({ID}) to cancel.", subscriber.GatewaySubscriptionId);
|
|
||||||
|
|
||||||
throw ContactSupport();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,92 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Repositories;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Stripe;
|
||||||
|
using static Bit.Core.Billing.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Queries.Implementations;
|
||||||
|
|
||||||
|
public class ProviderBillingQueries(
|
||||||
|
ILogger<ProviderBillingQueries> logger,
|
||||||
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
|
IProviderPlanRepository providerPlanRepository,
|
||||||
|
IProviderRepository providerRepository,
|
||||||
|
ISubscriberQueries subscriberQueries) : IProviderBillingQueries
|
||||||
|
{
|
||||||
|
public async Task<int> GetAssignedSeatTotalForPlanOrThrow(
|
||||||
|
Guid providerId,
|
||||||
|
PlanType planType)
|
||||||
|
{
|
||||||
|
var provider = await providerRepository.GetByIdAsync(providerId);
|
||||||
|
|
||||||
|
if (provider == null)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
"Could not find provider ({ID}) when retrieving assigned seat total",
|
||||||
|
providerId);
|
||||||
|
|
||||||
|
throw ContactSupport();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.Type == ProviderType.Reseller)
|
||||||
|
{
|
||||||
|
logger.LogError("Assigned seats cannot be retrieved for reseller-type provider ({ID})", providerId);
|
||||||
|
|
||||||
|
throw ContactSupport("Consolidated billing does not support reseller-type providers");
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);
|
||||||
|
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
|
||||||
|
return providerOrganizations
|
||||||
|
.Where(providerOrganization => providerOrganization.Plan == plan.Name)
|
||||||
|
.Sum(providerOrganization => providerOrganization.Seats ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProviderSubscriptionData> GetSubscriptionData(Guid providerId)
|
||||||
|
{
|
||||||
|
var provider = await providerRepository.GetByIdAsync(providerId);
|
||||||
|
|
||||||
|
if (provider == null)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
"Could not find provider ({ID}) when retrieving subscription data.",
|
||||||
|
providerId);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.Type == ProviderType.Reseller)
|
||||||
|
{
|
||||||
|
logger.LogError("Subscription data cannot be retrieved for reseller-type provider ({ID})", providerId);
|
||||||
|
|
||||||
|
throw ContactSupport("Consolidated billing does not support reseller-type providers");
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await subscriberQueries.GetSubscription(provider, new SubscriptionGetOptions
|
||||||
|
{
|
||||||
|
Expand = ["customer"]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerPlans = await providerPlanRepository.GetByProviderId(providerId);
|
||||||
|
|
||||||
|
var configuredProviderPlans = providerPlans
|
||||||
|
.Where(providerPlan => providerPlan.IsConfigured())
|
||||||
|
.Select(ConfiguredProviderPlan.From)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new ProviderSubscriptionData(
|
||||||
|
configuredProviderPlans,
|
||||||
|
subscription);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
using static Bit.Core.Billing.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Queries.Implementations;
|
||||||
|
|
||||||
|
public class SubscriberQueries(
|
||||||
|
ILogger<SubscriberQueries> logger,
|
||||||
|
IStripeAdapter stripeAdapter) : ISubscriberQueries
|
||||||
|
{
|
||||||
|
public async Task<Subscription> GetSubscription(
|
||||||
|
ISubscriber subscriber,
|
||||||
|
SubscriptionGetOptions subscriptionGetOptions = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(subscriber);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
|
||||||
|
{
|
||||||
|
logger.LogError("Cannot cancel subscription for subscriber ({ID}) with no GatewaySubscriptionId.", subscriber.Id);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions);
|
||||||
|
|
||||||
|
if (subscription != null)
|
||||||
|
{
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogError("Could not find Stripe subscription ({ID}) to cancel.", subscriber.GatewaySubscriptionId);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Subscription> GetSubscriptionOrThrow(ISubscriber subscriber)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(subscriber);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
|
||||||
|
{
|
||||||
|
logger.LogError("Cannot cancel subscription for subscriber ({ID}) with no GatewaySubscriptionId.", subscriber.Id);
|
||||||
|
|
||||||
|
throw ContactSupport();
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
|
||||||
|
|
||||||
|
if (subscription != null)
|
||||||
|
{
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogError("Could not find Stripe subscription ({ID}) to cancel.", subscriber.GatewaySubscriptionId);
|
||||||
|
|
||||||
|
throw ContactSupport();
|
||||||
|
}
|
||||||
|
}
|
@ -5,5 +5,5 @@ namespace Bit.Core.Billing.Repositories;
|
|||||||
|
|
||||||
public interface IProviderPlanRepository : IRepository<ProviderPlan, Guid>
|
public interface IProviderPlanRepository : IRepository<ProviderPlan, Guid>
|
||||||
{
|
{
|
||||||
Task<ProviderPlan> GetByProviderId(Guid providerId);
|
Task<ICollection<ProviderPlan>> GetByProviderId(Guid providerId);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
using Bit.Core.Exceptions;
|
namespace Bit.Core.Billing;
|
||||||
|
|
||||||
namespace Bit.Core.Billing;
|
|
||||||
|
|
||||||
public static class Utilities
|
public static class Utilities
|
||||||
{
|
{
|
||||||
public static GatewayException ContactSupport() => new("Something went wrong with your request. Please contact support.");
|
public const string BraintreeCustomerIdKey = "btCustomerId";
|
||||||
|
|
||||||
|
public static BillingException ContactSupport(
|
||||||
|
string internalMessage = null,
|
||||||
|
Exception innerException = null) => new("Something went wrong with your request. Please contact support.",
|
||||||
|
internalMessage, innerException);
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,6 @@ public static class FeatureFlagKeys
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FlexibleCollections = "flexible-collections-disabled-do-not-use";
|
public const string FlexibleCollections = "flexible-collections-disabled-do-not-use";
|
||||||
public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional
|
public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional
|
||||||
public const string BulkCollectionAccess = "bulk-collection-access";
|
|
||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
public const string KeyRotationImprovements = "key-rotation-improvements";
|
public const string KeyRotationImprovements = "key-rotation-improvements";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
@ -131,6 +130,9 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
||||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||||
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";
|
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";
|
||||||
|
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
||||||
|
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||||
|
public const string UnassignedItemsBanner = "unassigned-items-banner";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -21,8 +21,9 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.59" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.63" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.59" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.63" />
|
||||||
|
<PackageReference Include="Azure.Data.Tables" Version="12.8.3" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
|
||||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
|
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
|
||||||
@ -35,9 +36,8 @@
|
|||||||
<PackageReference Include="MailKit" Version="4.4.0" />
|
<PackageReference Include="MailKit" Version="4.4.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
||||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.38.0" />
|
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.38.0" />
|
||||||
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||||
@ -50,10 +50,10 @@
|
|||||||
<PackageReference Include="Sentry.Serilog" Version="3.41.4" />
|
<PackageReference Include="Sentry.Serilog" Version="3.41.4" />
|
||||||
<PackageReference Include="Duende.IdentityServer" Version="6.3.7" />
|
<PackageReference Include="Duende.IdentityServer" Version="6.3.7" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.1" />
|
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.2" />
|
||||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||||
<PackageReference Include="Braintree" Version="5.23.0" />
|
<PackageReference Include="Braintree" Version="5.23.0" />
|
||||||
<PackageReference Include="Stripe.net" Version="43.7.0" />
|
<PackageReference Include="Stripe.net" Version="43.20.0" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
||||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" />
|
||||||
|
11
src/Core/Enums/NotificationHubType.cs
Normal file
11
src/Core/Enums/NotificationHubType.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Bit.Core.Enums;
|
||||||
|
|
||||||
|
public enum NotificationHubType
|
||||||
|
{
|
||||||
|
General = 0,
|
||||||
|
Android = 1,
|
||||||
|
iOS = 2,
|
||||||
|
GeneralWeb = 3,
|
||||||
|
GeneralBrowserExtension = 4,
|
||||||
|
GeneralDesktop = 5
|
||||||
|
}
|
@ -28,14 +28,24 @@ public enum PlanType : byte
|
|||||||
EnterpriseMonthly2020 = 10,
|
EnterpriseMonthly2020 = 10,
|
||||||
[Display(Name = "Enterprise (Annually) 2020")]
|
[Display(Name = "Enterprise (Annually) 2020")]
|
||||||
EnterpriseAnnually2020 = 11,
|
EnterpriseAnnually2020 = 11,
|
||||||
|
[Display(Name = "Teams (Monthly) 2023")]
|
||||||
|
TeamsMonthly2023 = 12,
|
||||||
|
[Display(Name = "Teams (Annually) 2023")]
|
||||||
|
TeamsAnnually2023 = 13,
|
||||||
|
[Display(Name = "Enterprise (Monthly) 2023")]
|
||||||
|
EnterpriseMonthly2023 = 14,
|
||||||
|
[Display(Name = "Enterprise (Annually) 2023")]
|
||||||
|
EnterpriseAnnually2023 = 15,
|
||||||
|
[Display(Name = "Teams Starter 2023")]
|
||||||
|
TeamsStarter2023 = 16,
|
||||||
[Display(Name = "Teams (Monthly)")]
|
[Display(Name = "Teams (Monthly)")]
|
||||||
TeamsMonthly = 12,
|
TeamsMonthly = 17,
|
||||||
[Display(Name = "Teams (Annually)")]
|
[Display(Name = "Teams (Annually)")]
|
||||||
TeamsAnnually = 13,
|
TeamsAnnually = 18,
|
||||||
[Display(Name = "Enterprise (Monthly)")]
|
[Display(Name = "Enterprise (Monthly)")]
|
||||||
EnterpriseMonthly = 14,
|
EnterpriseMonthly = 19,
|
||||||
[Display(Name = "Enterprise (Annually)")]
|
[Display(Name = "Enterprise (Annually)")]
|
||||||
EnterpriseAnnually = 15,
|
EnterpriseAnnually = 20,
|
||||||
[Display(Name = "Teams Starter")]
|
[Display(Name = "Teams Starter")]
|
||||||
TeamsStarter = 16,
|
TeamsStarter = 21,
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<td class="content-block"
|
<td class="content-block"
|
||||||
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;"
|
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;"
|
||||||
valign="top">
|
valign="top">
|
||||||
Your organization has reached the Secrets Manager service accounts limit of {{MaxServiceAccountsCount}}. New service accounts cannot be created
|
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{{#>BasicTextLayout}}
|
{{#>BasicTextLayout}}
|
||||||
Your organization has reached the Secrets Manager service accounts limit of {{MaxServiceAccountsCount}}. New service accounts cannot be created
|
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created
|
||||||
|
|
||||||
For more information, please refer to the following help article: https://bitwarden.com/help/managing-users
|
For more information, please refer to the following help article: https://bitwarden.com/help/managing-users
|
||||||
{{/BasicTextLayout}}
|
{{/BasicTextLayout}}
|
||||||
|
12
src/Core/Models/Api/Request/PushDeviceRequestModel.cs
Normal file
12
src/Core/Models/Api/Request/PushDeviceRequestModel.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Api;
|
||||||
|
|
||||||
|
public class PushDeviceRequestModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[Required]
|
||||||
|
public DeviceType Type { get; set; }
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api;
|
namespace Bit.Core.Models.Api;
|
||||||
|
|
||||||
@ -7,14 +8,14 @@ public class PushUpdateRequestModel
|
|||||||
public PushUpdateRequestModel()
|
public PushUpdateRequestModel()
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public PushUpdateRequestModel(IEnumerable<string> deviceIds, string organizationId)
|
public PushUpdateRequestModel(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||||
{
|
{
|
||||||
DeviceIds = deviceIds;
|
Devices = devices.Select(d => new PushDeviceRequestModel { Id = d.Key, Type = d.Value });
|
||||||
OrganizationId = organizationId;
|
OrganizationId = organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public IEnumerable<string> DeviceIds { get; set; }
|
public IEnumerable<PushDeviceRequestModel> Devices { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public string OrganizationId { get; set; }
|
public string OrganizationId { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
@ -279,25 +278,6 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SubscriptionItem FindSubscriptionItem(Subscription subscription, string planId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(planId))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = subscription.Items.Data;
|
|
||||||
|
|
||||||
var subscriptionItem = data.FirstOrDefault(item => item.Plan?.Id == planId) ?? data.FirstOrDefault(item => item.Price?.Id == planId);
|
|
||||||
|
|
||||||
return subscriptionItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetPasswordManagerPlanId(StaticStore.Plan plan)
|
|
||||||
=> IsNonSeatBasedPlan(plan)
|
|
||||||
? plan.PasswordManager.StripePlanId
|
|
||||||
: plan.PasswordManager.StripeSeatPlanId;
|
|
||||||
|
|
||||||
private static SubscriptionData GetSubscriptionDataFor(Organization organization)
|
private static SubscriptionData GetSubscriptionDataFor(Organization organization)
|
||||||
{
|
{
|
||||||
var plan = Utilities.StaticStore.GetPlan(organization.PlanType);
|
var plan = Utilities.StaticStore.GetPlan(organization.PlanType);
|
||||||
@ -320,10 +300,4 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
|
|||||||
0
|
0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsNonSeatBasedPlan(StaticStore.Plan plan)
|
|
||||||
=> plan.Type is
|
|
||||||
>= PlanType.FamiliesAnnually2019 and <= PlanType.EnterpriseAnnually2019
|
|
||||||
or PlanType.FamiliesAnnually
|
|
||||||
or PlanType.TeamsStarter;
|
|
||||||
}
|
}
|
||||||
|
61
src/Core/Models/Business/ProviderSubscriptionUpdate.cs
Normal file
61
src/Core/Models/Business/ProviderSubscriptionUpdate.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
using static Bit.Core.Billing.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Business;
|
||||||
|
|
||||||
|
public class ProviderSubscriptionUpdate : SubscriptionUpdate
|
||||||
|
{
|
||||||
|
private readonly string _planId;
|
||||||
|
private readonly int _previouslyPurchasedSeats;
|
||||||
|
private readonly int _newlyPurchasedSeats;
|
||||||
|
|
||||||
|
protected override List<string> PlanIds => [_planId];
|
||||||
|
|
||||||
|
public ProviderSubscriptionUpdate(
|
||||||
|
PlanType planType,
|
||||||
|
int previouslyPurchasedSeats,
|
||||||
|
int newlyPurchasedSeats)
|
||||||
|
{
|
||||||
|
if (!planType.SupportsConsolidatedBilling())
|
||||||
|
{
|
||||||
|
throw ContactSupport($"Cannot create a {nameof(ProviderSubscriptionUpdate)} for {nameof(PlanType)} that doesn't support consolidated billing");
|
||||||
|
}
|
||||||
|
|
||||||
|
_planId = GetPasswordManagerPlanId(Utilities.StaticStore.GetPlan(planType));
|
||||||
|
_previouslyPurchasedSeats = previouslyPurchasedSeats;
|
||||||
|
_newlyPurchasedSeats = newlyPurchasedSeats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
||||||
|
{
|
||||||
|
var subscriptionItem = FindSubscriptionItem(subscription, _planId);
|
||||||
|
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = _planId,
|
||||||
|
Quantity = _previouslyPurchasedSeats
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||||
|
{
|
||||||
|
var subscriptionItem = FindSubscriptionItem(subscription, _planId);
|
||||||
|
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = _planId,
|
||||||
|
Quantity = _newlyPurchasedSeats
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@ public class SeatSubscriptionUpdate : SubscriptionUpdate
|
|||||||
|
|
||||||
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||||
{
|
{
|
||||||
var item = SubscriptionItem(subscription, PlanIds.Single());
|
var item = FindSubscriptionItem(subscription, PlanIds.Single());
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions
|
||||||
@ -34,7 +34,7 @@ public class SeatSubscriptionUpdate : SubscriptionUpdate
|
|||||||
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
||||||
{
|
{
|
||||||
|
|
||||||
var item = SubscriptionItem(subscription, PlanIds.Single());
|
var item = FindSubscriptionItem(subscription, PlanIds.Single());
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions
|
||||||
|
@ -19,7 +19,7 @@ public class ServiceAccountSubscriptionUpdate : SubscriptionUpdate
|
|||||||
|
|
||||||
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||||
{
|
{
|
||||||
var item = SubscriptionItem(subscription, PlanIds.Single());
|
var item = FindSubscriptionItem(subscription, PlanIds.Single());
|
||||||
_prevServiceAccounts = item?.Quantity ?? 0;
|
_prevServiceAccounts = item?.Quantity ?? 0;
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@ -35,7 +35,7 @@ public class ServiceAccountSubscriptionUpdate : SubscriptionUpdate
|
|||||||
|
|
||||||
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
||||||
{
|
{
|
||||||
var item = SubscriptionItem(subscription, PlanIds.Single());
|
var item = FindSubscriptionItem(subscription, PlanIds.Single());
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions
|
||||||
|
@ -19,7 +19,7 @@ public class SmSeatSubscriptionUpdate : SubscriptionUpdate
|
|||||||
|
|
||||||
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||||
{
|
{
|
||||||
var item = SubscriptionItem(subscription, PlanIds.Single());
|
var item = FindSubscriptionItem(subscription, PlanIds.Single());
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions
|
||||||
@ -35,7 +35,7 @@ public class SmSeatSubscriptionUpdate : SubscriptionUpdate
|
|||||||
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
||||||
{
|
{
|
||||||
|
|
||||||
var item = SubscriptionItem(subscription, PlanIds.Single());
|
var item = FindSubscriptionItem(subscription, PlanIds.Single());
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions
|
||||||
|
@ -74,10 +74,10 @@ public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate
|
|||||||
private string AddStripePlanId => _applySponsorship ? _sponsoredPlanStripeId : _existingPlanStripeId;
|
private string AddStripePlanId => _applySponsorship ? _sponsoredPlanStripeId : _existingPlanStripeId;
|
||||||
private Stripe.SubscriptionItem RemoveStripeItem(Subscription subscription) =>
|
private Stripe.SubscriptionItem RemoveStripeItem(Subscription subscription) =>
|
||||||
_applySponsorship ?
|
_applySponsorship ?
|
||||||
SubscriptionItem(subscription, _existingPlanStripeId) :
|
FindSubscriptionItem(subscription, _existingPlanStripeId) :
|
||||||
SubscriptionItem(subscription, _sponsoredPlanStripeId);
|
FindSubscriptionItem(subscription, _sponsoredPlanStripeId);
|
||||||
private Stripe.SubscriptionItem AddStripeItem(Subscription subscription) =>
|
private Stripe.SubscriptionItem AddStripeItem(Subscription subscription) =>
|
||||||
_applySponsorship ?
|
_applySponsorship ?
|
||||||
SubscriptionItem(subscription, _sponsoredPlanStripeId) :
|
FindSubscriptionItem(subscription, _sponsoredPlanStripeId) :
|
||||||
SubscriptionItem(subscription, _existingPlanStripeId);
|
FindSubscriptionItem(subscription, _existingPlanStripeId);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ public class StorageSubscriptionUpdate : SubscriptionUpdate
|
|||||||
|
|
||||||
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||||
{
|
{
|
||||||
var item = SubscriptionItem(subscription, PlanIds.Single());
|
var item = FindSubscriptionItem(subscription, PlanIds.Single());
|
||||||
_prevStorage = item?.Quantity ?? 0;
|
_prevStorage = item?.Quantity ?? 0;
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@ -38,7 +38,7 @@ public class StorageSubscriptionUpdate : SubscriptionUpdate
|
|||||||
throw new Exception("Unknown previous value, must first call UpgradeItemsOptions");
|
throw new Exception("Unknown previous value, must first call UpgradeItemsOptions");
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = SubscriptionItem(subscription, PlanIds.Single());
|
var item = FindSubscriptionItem(subscription, PlanIds.Single());
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions
|
||||||
|
@ -43,6 +43,9 @@ public class SubscriptionInfo
|
|||||||
Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));
|
Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));
|
||||||
}
|
}
|
||||||
CollectionMethod = sub.CollectionMethod;
|
CollectionMethod = sub.CollectionMethod;
|
||||||
|
GracePeriod = sub.CollectionMethod == "charge_automatically"
|
||||||
|
? 14
|
||||||
|
: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime? TrialStartDate { get; set; }
|
public DateTime? TrialStartDate { get; set; }
|
||||||
@ -56,6 +59,9 @@ public class SubscriptionInfo
|
|||||||
public bool Cancelled { get; set; }
|
public bool Cancelled { get; set; }
|
||||||
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
||||||
public string CollectionMethod { get; set; }
|
public string CollectionMethod { get; set; }
|
||||||
|
public DateTime? SuspensionDate { get; set; }
|
||||||
|
public DateTime? UnpaidPeriodEndDate { get; set; }
|
||||||
|
public int GracePeriod { get; set; }
|
||||||
|
|
||||||
public class BillingSubscriptionItem
|
public class BillingSubscriptionItem
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Stripe;
|
using Bit.Core.Enums;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Business;
|
namespace Bit.Core.Models.Business;
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ public abstract class SubscriptionUpdate
|
|||||||
foreach (var upgradeItemOptions in upgradeItemsOptions)
|
foreach (var upgradeItemOptions in upgradeItemsOptions)
|
||||||
{
|
{
|
||||||
var upgradeQuantity = upgradeItemOptions.Quantity ?? 0;
|
var upgradeQuantity = upgradeItemOptions.Quantity ?? 0;
|
||||||
var existingQuantity = SubscriptionItem(subscription, upgradeItemOptions.Plan)?.Quantity ?? 0;
|
var existingQuantity = FindSubscriptionItem(subscription, upgradeItemOptions.Plan)?.Quantity ?? 0;
|
||||||
if (upgradeQuantity != existingQuantity)
|
if (upgradeQuantity != existingQuantity)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@ -24,6 +25,28 @@ public abstract class SubscriptionUpdate
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static SubscriptionItem SubscriptionItem(Subscription subscription, string planId) =>
|
protected static SubscriptionItem FindSubscriptionItem(Subscription subscription, string planId)
|
||||||
planId == null ? null : subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId);
|
{
|
||||||
|
if (string.IsNullOrEmpty(planId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = subscription.Items.Data;
|
||||||
|
|
||||||
|
var subscriptionItem = data.FirstOrDefault(item => item.Plan?.Id == planId) ?? data.FirstOrDefault(item => item.Price?.Id == planId);
|
||||||
|
|
||||||
|
return subscriptionItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static string GetPasswordManagerPlanId(StaticStore.Plan plan)
|
||||||
|
=> IsNonSeatBasedPlan(plan)
|
||||||
|
? plan.PasswordManager.StripePlanId
|
||||||
|
: plan.PasswordManager.StripeSeatPlanId;
|
||||||
|
|
||||||
|
protected static bool IsNonSeatBasedPlan(StaticStore.Plan plan)
|
||||||
|
=> plan.Type is
|
||||||
|
>= PlanType.FamiliesAnnually2019 and <= PlanType.EnterpriseAnnually2019
|
||||||
|
or PlanType.FamiliesAnnually
|
||||||
|
or PlanType.TeamsStarter;
|
||||||
}
|
}
|
||||||
|
@ -1,134 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using Microsoft.Azure.Cosmos.Table;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Data;
|
|
||||||
|
|
||||||
public class DictionaryEntity : TableEntity, IDictionary<string, EntityProperty>
|
|
||||||
{
|
|
||||||
private IDictionary<string, EntityProperty> _properties = new Dictionary<string, EntityProperty>();
|
|
||||||
|
|
||||||
public ICollection<EntityProperty> Values => _properties.Values;
|
|
||||||
|
|
||||||
public EntityProperty this[string key]
|
|
||||||
{
|
|
||||||
get => _properties[key];
|
|
||||||
set => _properties[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Count => _properties.Count;
|
|
||||||
|
|
||||||
public bool IsReadOnly => _properties.IsReadOnly;
|
|
||||||
|
|
||||||
public ICollection<string> Keys => _properties.Keys;
|
|
||||||
|
|
||||||
public override void ReadEntity(IDictionary<string, EntityProperty> properties,
|
|
||||||
OperationContext operationContext)
|
|
||||||
{
|
|
||||||
_properties = properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
|
|
||||||
{
|
|
||||||
return _properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, EntityProperty value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, bool value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, byte[] value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, DateTime? value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, DateTimeOffset? value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, double value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, Guid value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, int value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, long value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, string value)
|
|
||||||
{
|
|
||||||
_properties.Add(key, new EntityProperty(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(KeyValuePair<string, EntityProperty> item)
|
|
||||||
{
|
|
||||||
_properties.Add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ContainsKey(string key)
|
|
||||||
{
|
|
||||||
return _properties.ContainsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Remove(string key)
|
|
||||||
{
|
|
||||||
return _properties.Remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetValue(string key, out EntityProperty value)
|
|
||||||
{
|
|
||||||
return _properties.TryGetValue(key, out value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
_properties.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(KeyValuePair<string, EntityProperty> item)
|
|
||||||
{
|
|
||||||
return _properties.Contains(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CopyTo(KeyValuePair<string, EntityProperty>[] array, int arrayIndex)
|
|
||||||
{
|
|
||||||
_properties.CopyTo(array, arrayIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Remove(KeyValuePair<string, EntityProperty> item)
|
|
||||||
{
|
|
||||||
return _properties.Remove(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<string, EntityProperty>> GetEnumerator()
|
|
||||||
{
|
|
||||||
return _properties.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return _properties.GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,73 @@
|
|||||||
using Bit.Core.Enums;
|
using Azure;
|
||||||
|
using Azure.Data.Tables;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.Azure.Cosmos.Table;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Data;
|
namespace Bit.Core.Models.Data;
|
||||||
|
|
||||||
public class EventTableEntity : TableEntity, IEvent
|
// used solely for interaction with Azure Table Storage
|
||||||
|
public class AzureEvent : ITableEntity
|
||||||
|
{
|
||||||
|
public string PartitionKey { get; set; }
|
||||||
|
public string RowKey { get; set; }
|
||||||
|
public DateTimeOffset? Timestamp { get; set; }
|
||||||
|
public ETag ETag { get; set; }
|
||||||
|
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public int Type { get; set; }
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
public Guid? OrganizationId { get; set; }
|
||||||
|
public Guid? InstallationId { get; set; }
|
||||||
|
public Guid? ProviderId { get; set; }
|
||||||
|
public Guid? CipherId { get; set; }
|
||||||
|
public Guid? CollectionId { get; set; }
|
||||||
|
public Guid? PolicyId { get; set; }
|
||||||
|
public Guid? GroupId { get; set; }
|
||||||
|
public Guid? OrganizationUserId { get; set; }
|
||||||
|
public Guid? ProviderUserId { get; set; }
|
||||||
|
public Guid? ProviderOrganizationId { get; set; }
|
||||||
|
public int? DeviceType { get; set; }
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
public Guid? ActingUserId { get; set; }
|
||||||
|
public int? SystemUser { get; set; }
|
||||||
|
public string DomainName { get; set; }
|
||||||
|
public Guid? SecretId { get; set; }
|
||||||
|
public Guid? ServiceAccountId { get; set; }
|
||||||
|
|
||||||
|
public EventTableEntity ToEventTableEntity()
|
||||||
|
{
|
||||||
|
return new EventTableEntity
|
||||||
|
{
|
||||||
|
PartitionKey = PartitionKey,
|
||||||
|
RowKey = RowKey,
|
||||||
|
Timestamp = Timestamp,
|
||||||
|
ETag = ETag,
|
||||||
|
|
||||||
|
Date = Date,
|
||||||
|
Type = (EventType)Type,
|
||||||
|
UserId = UserId,
|
||||||
|
OrganizationId = OrganizationId,
|
||||||
|
InstallationId = InstallationId,
|
||||||
|
ProviderId = ProviderId,
|
||||||
|
CipherId = CipherId,
|
||||||
|
CollectionId = CollectionId,
|
||||||
|
PolicyId = PolicyId,
|
||||||
|
GroupId = GroupId,
|
||||||
|
OrganizationUserId = OrganizationUserId,
|
||||||
|
ProviderUserId = ProviderUserId,
|
||||||
|
ProviderOrganizationId = ProviderOrganizationId,
|
||||||
|
DeviceType = DeviceType.HasValue ? (DeviceType)DeviceType.Value : null,
|
||||||
|
IpAddress = IpAddress,
|
||||||
|
ActingUserId = ActingUserId,
|
||||||
|
SystemUser = SystemUser.HasValue ? (EventSystemUser)SystemUser.Value : null,
|
||||||
|
DomainName = DomainName,
|
||||||
|
SecretId = SecretId,
|
||||||
|
ServiceAccountId = ServiceAccountId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EventTableEntity : IEvent
|
||||||
{
|
{
|
||||||
public EventTableEntity() { }
|
public EventTableEntity() { }
|
||||||
|
|
||||||
@ -32,6 +95,11 @@ public class EventTableEntity : TableEntity, IEvent
|
|||||||
ServiceAccountId = e.ServiceAccountId;
|
ServiceAccountId = e.ServiceAccountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string PartitionKey { get; set; }
|
||||||
|
public string RowKey { get; set; }
|
||||||
|
public DateTimeOffset? Timestamp { get; set; }
|
||||||
|
public ETag ETag { get; set; }
|
||||||
|
|
||||||
public DateTime Date { get; set; }
|
public DateTime Date { get; set; }
|
||||||
public EventType Type { get; set; }
|
public EventType Type { get; set; }
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
@ -53,65 +121,36 @@ public class EventTableEntity : TableEntity, IEvent
|
|||||||
public Guid? SecretId { get; set; }
|
public Guid? SecretId { get; set; }
|
||||||
public Guid? ServiceAccountId { get; set; }
|
public Guid? ServiceAccountId { get; set; }
|
||||||
|
|
||||||
public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
|
public AzureEvent ToAzureEvent()
|
||||||
{
|
{
|
||||||
var result = base.WriteEntity(operationContext);
|
return new AzureEvent
|
||||||
|
{
|
||||||
|
PartitionKey = PartitionKey,
|
||||||
|
RowKey = RowKey,
|
||||||
|
Timestamp = Timestamp,
|
||||||
|
ETag = ETag,
|
||||||
|
|
||||||
var typeName = nameof(Type);
|
Date = Date,
|
||||||
if (result.ContainsKey(typeName))
|
Type = (int)Type,
|
||||||
{
|
UserId = UserId,
|
||||||
result[typeName] = new EntityProperty((int)Type);
|
OrganizationId = OrganizationId,
|
||||||
}
|
InstallationId = InstallationId,
|
||||||
else
|
ProviderId = ProviderId,
|
||||||
{
|
CipherId = CipherId,
|
||||||
result.Add(typeName, new EntityProperty((int)Type));
|
CollectionId = CollectionId,
|
||||||
}
|
PolicyId = PolicyId,
|
||||||
|
GroupId = GroupId,
|
||||||
var deviceTypeName = nameof(DeviceType);
|
OrganizationUserId = OrganizationUserId,
|
||||||
if (result.ContainsKey(deviceTypeName))
|
ProviderUserId = ProviderUserId,
|
||||||
{
|
ProviderOrganizationId = ProviderOrganizationId,
|
||||||
result[deviceTypeName] = new EntityProperty((int?)DeviceType);
|
DeviceType = DeviceType.HasValue ? (int)DeviceType.Value : null,
|
||||||
}
|
IpAddress = IpAddress,
|
||||||
else
|
ActingUserId = ActingUserId,
|
||||||
{
|
SystemUser = SystemUser.HasValue ? (int)SystemUser.Value : null,
|
||||||
result.Add(deviceTypeName, new EntityProperty((int?)DeviceType));
|
DomainName = DomainName,
|
||||||
}
|
SecretId = SecretId,
|
||||||
|
ServiceAccountId = ServiceAccountId
|
||||||
var systemUserTypeName = nameof(SystemUser);
|
};
|
||||||
if (result.ContainsKey(systemUserTypeName))
|
|
||||||
{
|
|
||||||
result[systemUserTypeName] = new EntityProperty((int?)SystemUser);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.Add(systemUserTypeName, new EntityProperty((int?)SystemUser));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ReadEntity(IDictionary<string, EntityProperty> properties,
|
|
||||||
OperationContext operationContext)
|
|
||||||
{
|
|
||||||
base.ReadEntity(properties, operationContext);
|
|
||||||
|
|
||||||
var typeName = nameof(Type);
|
|
||||||
if (properties.ContainsKey(typeName) && properties[typeName].Int32Value.HasValue)
|
|
||||||
{
|
|
||||||
Type = (EventType)properties[typeName].Int32Value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var deviceTypeName = nameof(DeviceType);
|
|
||||||
if (properties.ContainsKey(deviceTypeName) && properties[deviceTypeName].Int32Value.HasValue)
|
|
||||||
{
|
|
||||||
DeviceType = (DeviceType)properties[deviceTypeName].Int32Value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemUserTypeName = nameof(SystemUser);
|
|
||||||
if (properties.ContainsKey(systemUserTypeName) && properties[systemUserTypeName].Int32Value.HasValue)
|
|
||||||
{
|
|
||||||
SystemUser = (EventSystemUser)properties[systemUserTypeName].Int32Value.Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<EventTableEntity> IndexEvent(EventMessage e)
|
public static List<EventTableEntity> IndexEvent(EventMessage e)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
using Microsoft.Azure.Cosmos.Table;
|
using Azure;
|
||||||
|
using Azure.Data.Tables;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Data;
|
namespace Bit.Core.Models.Data;
|
||||||
|
|
||||||
public class InstallationDeviceEntity : TableEntity
|
public class InstallationDeviceEntity : ITableEntity
|
||||||
{
|
{
|
||||||
public InstallationDeviceEntity() { }
|
public InstallationDeviceEntity() { }
|
||||||
|
|
||||||
@ -27,6 +28,11 @@ public class InstallationDeviceEntity : TableEntity
|
|||||||
RowKey = parts[1];
|
RowKey = parts[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string PartitionKey { get; set; }
|
||||||
|
public string RowKey { get; set; }
|
||||||
|
public DateTimeOffset? Timestamp { get; set; }
|
||||||
|
public ETag ETag { get; set; }
|
||||||
|
|
||||||
public static bool IsInstallationDeviceId(string deviceId)
|
public static bool IsInstallationDeviceId(string deviceId)
|
||||||
{
|
{
|
||||||
return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';
|
return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Bit.Core.Models.StaticStore.Plans;
|
namespace Bit.Core.Models.StaticStore.Plans;
|
||||||
|
|
||||||
public record EnterprisePlan : Models.StaticStore.Plan
|
public record EnterprisePlan : Plan
|
||||||
{
|
{
|
||||||
public EnterprisePlan(bool isAnnual)
|
public EnterprisePlan(bool isAnnual)
|
||||||
{
|
{
|
||||||
@ -44,7 +44,7 @@ public record EnterprisePlan : Models.StaticStore.Plan
|
|||||||
{
|
{
|
||||||
BaseSeats = 0;
|
BaseSeats = 0;
|
||||||
BasePrice = 0;
|
BasePrice = 0;
|
||||||
BaseServiceAccount = 200;
|
BaseServiceAccount = 50;
|
||||||
|
|
||||||
HasAdditionalSeatsOption = true;
|
HasAdditionalSeatsOption = true;
|
||||||
HasAdditionalServiceAccountOption = true;
|
HasAdditionalServiceAccountOption = true;
|
||||||
@ -55,16 +55,16 @@ public record EnterprisePlan : Models.StaticStore.Plan
|
|||||||
if (isAnnual)
|
if (isAnnual)
|
||||||
{
|
{
|
||||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
|
||||||
SeatPrice = 144;
|
SeatPrice = 144;
|
||||||
AdditionalPricePerServiceAccount = 6;
|
AdditionalPricePerServiceAccount = 12;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||||
SeatPrice = 13;
|
SeatPrice = 13;
|
||||||
AdditionalPricePerServiceAccount = 0.5M;
|
AdditionalPricePerServiceAccount = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
102
src/Core/Models/StaticStore/Plans/EnterprisePlan2023.cs
Normal file
102
src/Core/Models/StaticStore/Plans/EnterprisePlan2023.cs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.StaticStore.Plans;
|
||||||
|
|
||||||
|
public record Enterprise2023Plan : Plan
|
||||||
|
{
|
||||||
|
public Enterprise2023Plan(bool isAnnual)
|
||||||
|
{
|
||||||
|
Type = isAnnual ? PlanType.EnterpriseAnnually2023 : PlanType.EnterpriseMonthly2023;
|
||||||
|
Product = ProductType.Enterprise;
|
||||||
|
Name = isAnnual ? "Enterprise (Annually)" : "Enterprise (Monthly)";
|
||||||
|
IsAnnual = isAnnual;
|
||||||
|
NameLocalizationKey = "planNameEnterprise";
|
||||||
|
DescriptionLocalizationKey = "planDescEnterprise";
|
||||||
|
CanBeUsedByBusiness = true;
|
||||||
|
|
||||||
|
TrialPeriodDays = 7;
|
||||||
|
|
||||||
|
HasPolicies = true;
|
||||||
|
HasSelfHost = true;
|
||||||
|
HasGroups = true;
|
||||||
|
HasDirectory = true;
|
||||||
|
HasEvents = true;
|
||||||
|
HasTotp = true;
|
||||||
|
Has2fa = true;
|
||||||
|
HasApi = true;
|
||||||
|
HasSso = true;
|
||||||
|
HasKeyConnector = true;
|
||||||
|
HasScim = true;
|
||||||
|
HasResetPassword = true;
|
||||||
|
UsersGetPremium = true;
|
||||||
|
HasCustomPermissions = true;
|
||||||
|
|
||||||
|
UpgradeSortOrder = 4;
|
||||||
|
DisplaySortOrder = 4;
|
||||||
|
|
||||||
|
LegacyYear = 2024;
|
||||||
|
|
||||||
|
PasswordManager = new Enterprise2023PasswordManagerFeatures(isAnnual);
|
||||||
|
SecretsManager = new Enterprise2023SecretsManagerFeatures(isAnnual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Enterprise2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Enterprise2023SecretsManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BasePrice = 0;
|
||||||
|
BaseServiceAccount = 200;
|
||||||
|
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
HasAdditionalServiceAccountOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
AllowServiceAccountsAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||||
|
SeatPrice = 144;
|
||||||
|
AdditionalPricePerServiceAccount = 6;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||||
|
SeatPrice = 13;
|
||||||
|
AdditionalPricePerServiceAccount = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Enterprise2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Enterprise2023PasswordManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BaseStorageGb = 1;
|
||||||
|
|
||||||
|
HasAdditionalStorageOption = true;
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
AdditionalStoragePricePerGb = 4;
|
||||||
|
StripeStoragePlanId = "storage-gb-annually";
|
||||||
|
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||||
|
SeatPrice = 72;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "2023-enterprise-seat-monthly";
|
||||||
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
|
SeatPrice = 7;
|
||||||
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Bit.Core.Models.StaticStore.Plans;
|
namespace Bit.Core.Models.StaticStore.Plans;
|
||||||
|
|
||||||
public record TeamsPlan : Models.StaticStore.Plan
|
public record TeamsPlan : Plan
|
||||||
{
|
{
|
||||||
public TeamsPlan(bool isAnnual)
|
public TeamsPlan(bool isAnnual)
|
||||||
{
|
{
|
||||||
@ -37,7 +37,7 @@ public record TeamsPlan : Models.StaticStore.Plan
|
|||||||
{
|
{
|
||||||
BaseSeats = 0;
|
BaseSeats = 0;
|
||||||
BasePrice = 0;
|
BasePrice = 0;
|
||||||
BaseServiceAccount = 50;
|
BaseServiceAccount = 20;
|
||||||
|
|
||||||
HasAdditionalSeatsOption = true;
|
HasAdditionalSeatsOption = true;
|
||||||
HasAdditionalServiceAccountOption = true;
|
HasAdditionalServiceAccountOption = true;
|
||||||
@ -48,16 +48,16 @@ public record TeamsPlan : Models.StaticStore.Plan
|
|||||||
if (isAnnual)
|
if (isAnnual)
|
||||||
{
|
{
|
||||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
|
||||||
SeatPrice = 72;
|
SeatPrice = 72;
|
||||||
AdditionalPricePerServiceAccount = 6;
|
AdditionalPricePerServiceAccount = 12;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||||
SeatPrice = 7;
|
SeatPrice = 7;
|
||||||
AdditionalPricePerServiceAccount = 0.5M;
|
AdditionalPricePerServiceAccount = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
96
src/Core/Models/StaticStore/Plans/TeamsPlan2023.cs
Normal file
96
src/Core/Models/StaticStore/Plans/TeamsPlan2023.cs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.StaticStore.Plans;
|
||||||
|
|
||||||
|
public record Teams2023Plan : Plan
|
||||||
|
{
|
||||||
|
public Teams2023Plan(bool isAnnual)
|
||||||
|
{
|
||||||
|
Type = isAnnual ? PlanType.TeamsAnnually2023 : PlanType.TeamsMonthly2023;
|
||||||
|
Product = ProductType.Teams;
|
||||||
|
Name = isAnnual ? "Teams (Annually)" : "Teams (Monthly)";
|
||||||
|
IsAnnual = isAnnual;
|
||||||
|
NameLocalizationKey = "planNameTeams";
|
||||||
|
DescriptionLocalizationKey = "planDescTeams";
|
||||||
|
CanBeUsedByBusiness = true;
|
||||||
|
|
||||||
|
TrialPeriodDays = 7;
|
||||||
|
|
||||||
|
HasGroups = true;
|
||||||
|
HasDirectory = true;
|
||||||
|
HasEvents = true;
|
||||||
|
HasTotp = true;
|
||||||
|
Has2fa = true;
|
||||||
|
HasApi = true;
|
||||||
|
UsersGetPremium = true;
|
||||||
|
|
||||||
|
UpgradeSortOrder = 3;
|
||||||
|
DisplaySortOrder = 3;
|
||||||
|
|
||||||
|
LegacyYear = 2024;
|
||||||
|
|
||||||
|
PasswordManager = new Teams2023PasswordManagerFeatures(isAnnual);
|
||||||
|
SecretsManager = new Teams2023SecretsManagerFeatures(isAnnual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Teams2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Teams2023SecretsManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BasePrice = 0;
|
||||||
|
BaseServiceAccount = 50;
|
||||||
|
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
HasAdditionalServiceAccountOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
AllowServiceAccountsAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||||
|
SeatPrice = 72;
|
||||||
|
AdditionalPricePerServiceAccount = 6;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||||
|
SeatPrice = 7;
|
||||||
|
AdditionalPricePerServiceAccount = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Teams2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Teams2023PasswordManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BaseStorageGb = 1;
|
||||||
|
BasePrice = 0;
|
||||||
|
|
||||||
|
HasAdditionalStorageOption = true;
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
StripeStoragePlanId = "storage-gb-annually";
|
||||||
|
StripeSeatPlanId = "2023-teams-org-seat-annually";
|
||||||
|
SeatPrice = 48;
|
||||||
|
AdditionalStoragePricePerGb = 4;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "2023-teams-org-seat-monthly";
|
||||||
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
|
SeatPrice = 5;
|
||||||
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -36,7 +36,7 @@ public record TeamsStarterPlan : Plan
|
|||||||
{
|
{
|
||||||
BaseSeats = 0;
|
BaseSeats = 0;
|
||||||
BasePrice = 0;
|
BasePrice = 0;
|
||||||
BaseServiceAccount = 50;
|
BaseServiceAccount = 20;
|
||||||
|
|
||||||
HasAdditionalSeatsOption = true;
|
HasAdditionalSeatsOption = true;
|
||||||
HasAdditionalServiceAccountOption = true;
|
HasAdditionalServiceAccountOption = true;
|
||||||
@ -45,9 +45,9 @@ public record TeamsStarterPlan : Plan
|
|||||||
AllowServiceAccountsAutoscale = true;
|
AllowServiceAccountsAutoscale = true;
|
||||||
|
|
||||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||||
SeatPrice = 7;
|
SeatPrice = 7;
|
||||||
AdditionalPricePerServiceAccount = 0.5M;
|
AdditionalPricePerServiceAccount = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
72
src/Core/Models/StaticStore/Plans/TeamsStarterPlan2023.cs
Normal file
72
src/Core/Models/StaticStore/Plans/TeamsStarterPlan2023.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.StaticStore.Plans;
|
||||||
|
|
||||||
|
public record TeamsStarterPlan2023 : Plan
|
||||||
|
{
|
||||||
|
public TeamsStarterPlan2023()
|
||||||
|
{
|
||||||
|
Type = PlanType.TeamsStarter2023;
|
||||||
|
Product = ProductType.TeamsStarter;
|
||||||
|
Name = "Teams (Starter)";
|
||||||
|
NameLocalizationKey = "planNameTeamsStarter";
|
||||||
|
DescriptionLocalizationKey = "planDescTeams";
|
||||||
|
CanBeUsedByBusiness = true;
|
||||||
|
|
||||||
|
TrialPeriodDays = 7;
|
||||||
|
|
||||||
|
HasGroups = true;
|
||||||
|
HasDirectory = true;
|
||||||
|
HasEvents = true;
|
||||||
|
HasTotp = true;
|
||||||
|
Has2fa = true;
|
||||||
|
HasApi = true;
|
||||||
|
UsersGetPremium = true;
|
||||||
|
|
||||||
|
UpgradeSortOrder = 2;
|
||||||
|
DisplaySortOrder = 2;
|
||||||
|
|
||||||
|
PasswordManager = new TeamsStarter2023PasswordManagerFeatures();
|
||||||
|
SecretsManager = new TeamsStarter2023SecretsManagerFeatures();
|
||||||
|
LegacyYear = 2024;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record TeamsStarter2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public TeamsStarter2023SecretsManagerFeatures()
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BasePrice = 0;
|
||||||
|
BaseServiceAccount = 50;
|
||||||
|
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
HasAdditionalServiceAccountOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
AllowServiceAccountsAutoscale = true;
|
||||||
|
|
||||||
|
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||||
|
SeatPrice = 7;
|
||||||
|
AdditionalPricePerServiceAccount = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record TeamsStarter2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public TeamsStarter2023PasswordManagerFeatures()
|
||||||
|
{
|
||||||
|
BaseSeats = 10;
|
||||||
|
BaseStorageGb = 1;
|
||||||
|
BasePrice = 20;
|
||||||
|
|
||||||
|
MaxSeats = 10;
|
||||||
|
|
||||||
|
HasAdditionalStorageOption = true;
|
||||||
|
|
||||||
|
StripePlanId = "teams-org-starter";
|
||||||
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -118,7 +118,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Error encountered notifying organization owners of service accounts limit reached.");
|
_logger.LogError(e, $"Error encountered notifying organization owners of machine accounts limit reached.");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -253,12 +253,12 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
|||||||
// Check if the organization has unlimited service accounts
|
// Check if the organization has unlimited service accounts
|
||||||
if (organization.SmServiceAccounts == null)
|
if (organization.SmServiceAccounts == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organization has no service accounts limit, no need to adjust service accounts");
|
throw new BadRequestException("Organization has no machine accounts limit, no need to adjust machine accounts");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value)
|
if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Cannot use autoscaling to subtract service accounts.");
|
throw new BadRequestException("Cannot use autoscaling to subtract machine accounts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check plan maximum service accounts
|
// Check plan maximum service accounts
|
||||||
@ -267,7 +267,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
|||||||
{
|
{
|
||||||
var planMaxServiceAccounts = plan.SecretsManager.BaseServiceAccount +
|
var planMaxServiceAccounts = plan.SecretsManager.BaseServiceAccount +
|
||||||
plan.SecretsManager.MaxAdditionalServiceAccount.GetValueOrDefault();
|
plan.SecretsManager.MaxAdditionalServiceAccount.GetValueOrDefault();
|
||||||
throw new BadRequestException($"You have reached the maximum number of service accounts ({planMaxServiceAccounts}) for this plan.");
|
throw new BadRequestException($"You have reached the maximum number of machine accounts ({planMaxServiceAccounts}) for this plan.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check autoscale maximum service accounts
|
// Check autoscale maximum service accounts
|
||||||
@ -275,21 +275,21 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
|||||||
update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value)
|
update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value)
|
||||||
{
|
{
|
||||||
var message = update.Autoscaling
|
var message = update.Autoscaling
|
||||||
? "Secrets Manager service account limit has been reached."
|
? "Secrets Manager machine account limit has been reached."
|
||||||
: "Cannot set max service accounts autoscaling below service account amount.";
|
: "Cannot set max machine accounts autoscaling below machine account amount.";
|
||||||
throw new BadRequestException(message);
|
throw new BadRequestException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check minimum service accounts included with plan
|
// Check minimum service accounts included with plan
|
||||||
if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts.Value)
|
if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts.Value)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} service accounts.");
|
throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} machine accounts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check minimum service accounts required by business logic
|
// Check minimum service accounts required by business logic
|
||||||
if (update.SmServiceAccounts.Value <= 0)
|
if (update.SmServiceAccounts.Value <= 0)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You must have at least 1 service account.");
|
throw new BadRequestException("You must have at least 1 machine account.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check minimum service accounts currently in use by the organization
|
// Check minimum service accounts currently in use by the organization
|
||||||
@ -298,8 +298,8 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
|||||||
var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
||||||
if (currentServiceAccounts > update.SmServiceAccounts)
|
if (currentServiceAccounts > update.SmServiceAccounts)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} service accounts. " +
|
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} machine accounts. " +
|
||||||
$"You cannot decrease your subscription below your current service account usage.");
|
$"You cannot decrease your subscription below your current machine account usage.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -346,18 +346,18 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
|||||||
if (update.SmServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value < update.SmServiceAccounts.Value)
|
if (update.SmServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value < update.SmServiceAccounts.Value)
|
||||||
{
|
{
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
$"Cannot set max service accounts autoscaling below current service accounts count.");
|
$"Cannot set max machine accounts autoscaling below current machine accounts count.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!plan.SecretsManager.AllowServiceAccountsAutoscale)
|
if (!plan.SecretsManager.AllowServiceAccountsAutoscale)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Your plan does not allow service accounts autoscaling.");
|
throw new BadRequestException("Your plan does not allow machine accounts autoscaling.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plan.SecretsManager.MaxServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value > plan.SecretsManager.MaxServiceAccounts)
|
if (plan.SecretsManager.MaxServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value > plan.SecretsManager.MaxServiceAccounts)
|
||||||
{
|
{
|
||||||
throw new BadRequestException(string.Concat(
|
throw new BadRequestException(string.Concat(
|
||||||
$"Your plan has a service account limit of {plan.SecretsManager.MaxServiceAccounts}, ",
|
$"Your plan has a machine account limit of {plan.SecretsManager.MaxServiceAccounts}, ",
|
||||||
$"but you have specified a max autoscale count of {update.MaxAutoscaleSmServiceAccounts}.",
|
$"but you have specified a max autoscale count of {update.MaxAutoscaleSmServiceAccounts}.",
|
||||||
"Reduce your max autoscale count."));
|
"Reduce your max autoscale count."));
|
||||||
}
|
}
|
||||||
|
@ -330,9 +330,9 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
if (currentServiceAccounts > newPlanServiceAccounts)
|
if (currentServiceAccounts > newPlanServiceAccounts)
|
||||||
{
|
{
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
$"Your organization currently has {currentServiceAccounts} service accounts. " +
|
$"Your organization currently has {currentServiceAccounts} machine accounts. " +
|
||||||
$"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} service accounts. " +
|
$"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} machine accounts. " +
|
||||||
"Remove some service accounts or increase your subscription.");
|
"Remove some machine accounts or increase your subscription.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
using Bit.Core.Models.Data;
|
using Azure.Data.Tables;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.Azure.Cosmos.Table;
|
|
||||||
|
|
||||||
namespace Bit.Core.Repositories.TableStorage;
|
namespace Bit.Core.Repositories.TableStorage;
|
||||||
|
|
||||||
public class EventRepository : IEventRepository
|
public class EventRepository : IEventRepository
|
||||||
{
|
{
|
||||||
private readonly CloudTable _table;
|
private readonly TableClient _tableClient;
|
||||||
|
|
||||||
public EventRepository(GlobalSettings globalSettings)
|
public EventRepository(GlobalSettings globalSettings)
|
||||||
: this(globalSettings.Events.ConnectionString)
|
: this(globalSettings.Events.ConnectionString)
|
||||||
@ -16,9 +16,8 @@ public class EventRepository : IEventRepository
|
|||||||
|
|
||||||
public EventRepository(string storageConnectionString)
|
public EventRepository(string storageConnectionString)
|
||||||
{
|
{
|
||||||
var storageAccount = CloudStorageAccount.Parse(storageConnectionString);
|
var tableClient = new TableServiceClient(storageConnectionString);
|
||||||
var tableClient = storageAccount.CreateCloudTableClient();
|
_tableClient = tableClient.GetTableClient("event");
|
||||||
_table = tableClient.GetTableReference("event");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedResult<IEvent>> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate,
|
public async Task<PagedResult<IEvent>> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate,
|
||||||
@ -76,7 +75,7 @@ public class EventRepository : IEventRepository
|
|||||||
throw new ArgumentException(nameof(e));
|
throw new ArgumentException(nameof(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
await CreateEntityAsync(entity);
|
await CreateEventAsync(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateManyAsync(IEnumerable<IEvent> e)
|
public async Task CreateManyAsync(IEnumerable<IEvent> e)
|
||||||
@ -99,7 +98,7 @@ public class EventRepository : IEventRepository
|
|||||||
var groupEntities = group.ToList();
|
var groupEntities = group.ToList();
|
||||||
if (groupEntities.Count == 1)
|
if (groupEntities.Count == 1)
|
||||||
{
|
{
|
||||||
await CreateEntityAsync(groupEntities.First());
|
await CreateEventAsync(groupEntities.First());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +106,7 @@ public class EventRepository : IEventRepository
|
|||||||
var iterations = groupEntities.Count / 100;
|
var iterations = groupEntities.Count / 100;
|
||||||
for (var i = 0; i <= iterations; i++)
|
for (var i = 0; i <= iterations; i++)
|
||||||
{
|
{
|
||||||
var batch = new TableBatchOperation();
|
var batch = new List<TableTransactionAction>();
|
||||||
var batchEntities = groupEntities.Skip(i * 100).Take(100);
|
var batchEntities = groupEntities.Skip(i * 100).Take(100);
|
||||||
if (!batchEntities.Any())
|
if (!batchEntities.Any())
|
||||||
{
|
{
|
||||||
@ -116,19 +115,15 @@ public class EventRepository : IEventRepository
|
|||||||
|
|
||||||
foreach (var entity in batchEntities)
|
foreach (var entity in batchEntities)
|
||||||
{
|
{
|
||||||
batch.InsertOrReplace(entity);
|
batch.Add(new TableTransactionAction(TableTransactionActionType.Add,
|
||||||
|
entity.ToAzureEvent()));
|
||||||
}
|
}
|
||||||
|
|
||||||
await _table.ExecuteBatchAsync(batch);
|
await _tableClient.SubmitTransactionAsync(batch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateEntityAsync(ITableEntity entity)
|
|
||||||
{
|
|
||||||
await _table.ExecuteAsync(TableOperation.InsertOrReplace(entity));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PagedResult<IEvent>> GetManyAsync(string partitionKey, string rowKey,
|
public async Task<PagedResult<IEvent>> GetManyAsync(string partitionKey, string rowKey,
|
||||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||||
{
|
{
|
||||||
@ -136,60 +131,28 @@ public class EventRepository : IEventRepository
|
|||||||
var end = CoreHelpers.DateTimeToTableStorageKey(endDate);
|
var end = CoreHelpers.DateTimeToTableStorageKey(endDate);
|
||||||
var filter = MakeFilter(partitionKey, string.Format(rowKey, start), string.Format(rowKey, end));
|
var filter = MakeFilter(partitionKey, string.Format(rowKey, start), string.Format(rowKey, end));
|
||||||
|
|
||||||
var query = new TableQuery<EventTableEntity>().Where(filter).Take(pageOptions.PageSize);
|
|
||||||
var result = new PagedResult<IEvent>();
|
var result = new PagedResult<IEvent>();
|
||||||
var continuationToken = DeserializeContinuationToken(pageOptions?.ContinuationToken);
|
var query = _tableClient.QueryAsync<AzureEvent>(filter, pageOptions.PageSize);
|
||||||
|
|
||||||
var queryResults = await _table.ExecuteQuerySegmentedAsync(query, continuationToken);
|
await using (var enumerator = query.AsPages(pageOptions?.ContinuationToken,
|
||||||
result.ContinuationToken = SerializeContinuationToken(queryResults.ContinuationToken);
|
pageOptions.PageSize).GetAsyncEnumerator())
|
||||||
result.Data.AddRange(queryResults.Results);
|
{
|
||||||
|
await enumerator.MoveNextAsync();
|
||||||
|
|
||||||
|
result.ContinuationToken = enumerator.Current.ContinuationToken;
|
||||||
|
result.Data.AddRange(enumerator.Current.Values.Select(e => e.ToEventTableEntity()));
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CreateEventAsync(EventTableEntity entity)
|
||||||
|
{
|
||||||
|
await _tableClient.UpsertEntityAsync(entity.ToAzureEvent());
|
||||||
|
}
|
||||||
|
|
||||||
private string MakeFilter(string partitionKey, string rowStart, string rowEnd)
|
private string MakeFilter(string partitionKey, string rowStart, string rowEnd)
|
||||||
{
|
{
|
||||||
var rowFilter = TableQuery.CombineFilters(
|
return $"PartitionKey eq '{partitionKey}' and RowKey le '{rowStart}' and RowKey ge '{rowEnd}'";
|
||||||
TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThanOrEqual, $"{rowStart}`"),
|
|
||||||
TableOperators.And,
|
|
||||||
TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, $"{rowEnd}_"));
|
|
||||||
|
|
||||||
return TableQuery.CombineFilters(
|
|
||||||
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey),
|
|
||||||
TableOperators.And,
|
|
||||||
rowFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string SerializeContinuationToken(TableContinuationToken token)
|
|
||||||
{
|
|
||||||
if (token == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Format("{0}__{1}__{2}__{3}", (int)token.TargetLocation, token.NextTableName,
|
|
||||||
token.NextPartitionKey, token.NextRowKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TableContinuationToken DeserializeContinuationToken(string token)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokenParts = token.Split(new string[] { "__" }, StringSplitOptions.None);
|
|
||||||
if (tokenParts.Length < 4 || !Enum.TryParse(tokenParts[0], out StorageLocation tLoc))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TableContinuationToken
|
|
||||||
{
|
|
||||||
TargetLocation = tLoc,
|
|
||||||
NextTableName = string.IsNullOrWhiteSpace(tokenParts[1]) ? null : tokenParts[1],
|
|
||||||
NextPartitionKey = string.IsNullOrWhiteSpace(tokenParts[2]) ? null : tokenParts[2],
|
|
||||||
NextRowKey = string.IsNullOrWhiteSpace(tokenParts[3]) ? null : tokenParts[3]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
using System.Net;
|
using Azure.Data.Tables;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Azure.Cosmos.Table;
|
|
||||||
|
|
||||||
namespace Bit.Core.Repositories.TableStorage;
|
namespace Bit.Core.Repositories.TableStorage;
|
||||||
|
|
||||||
public class InstallationDeviceRepository : IInstallationDeviceRepository
|
public class InstallationDeviceRepository : IInstallationDeviceRepository
|
||||||
{
|
{
|
||||||
private readonly CloudTable _table;
|
private readonly TableClient _tableClient;
|
||||||
|
|
||||||
public InstallationDeviceRepository(GlobalSettings globalSettings)
|
public InstallationDeviceRepository(GlobalSettings globalSettings)
|
||||||
: this(globalSettings.Events.ConnectionString)
|
: this(globalSettings.Events.ConnectionString)
|
||||||
@ -15,14 +14,13 @@ public class InstallationDeviceRepository : IInstallationDeviceRepository
|
|||||||
|
|
||||||
public InstallationDeviceRepository(string storageConnectionString)
|
public InstallationDeviceRepository(string storageConnectionString)
|
||||||
{
|
{
|
||||||
var storageAccount = CloudStorageAccount.Parse(storageConnectionString);
|
var tableClient = new TableServiceClient(storageConnectionString);
|
||||||
var tableClient = storageAccount.CreateCloudTableClient();
|
_tableClient = tableClient.GetTableClient("installationdevice");
|
||||||
_table = tableClient.GetTableReference("installationdevice");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpsertAsync(InstallationDeviceEntity entity)
|
public async Task UpsertAsync(InstallationDeviceEntity entity)
|
||||||
{
|
{
|
||||||
await _table.ExecuteAsync(TableOperation.InsertOrReplace(entity));
|
await _tableClient.UpsertEntityAsync(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpsertManyAsync(IList<InstallationDeviceEntity> entities)
|
public async Task UpsertManyAsync(IList<InstallationDeviceEntity> entities)
|
||||||
@ -52,7 +50,7 @@ public class InstallationDeviceRepository : IInstallationDeviceRepository
|
|||||||
var iterations = groupEntities.Count / 100;
|
var iterations = groupEntities.Count / 100;
|
||||||
for (var i = 0; i <= iterations; i++)
|
for (var i = 0; i <= iterations; i++)
|
||||||
{
|
{
|
||||||
var batch = new TableBatchOperation();
|
var batch = new List<TableTransactionAction>();
|
||||||
var batchEntities = groupEntities.Skip(i * 100).Take(100);
|
var batchEntities = groupEntities.Skip(i * 100).Take(100);
|
||||||
if (!batchEntities.Any())
|
if (!batchEntities.Any())
|
||||||
{
|
{
|
||||||
@ -61,24 +59,16 @@ public class InstallationDeviceRepository : IInstallationDeviceRepository
|
|||||||
|
|
||||||
foreach (var entity in batchEntities)
|
foreach (var entity in batchEntities)
|
||||||
{
|
{
|
||||||
batch.InsertOrReplace(entity);
|
batch.Add(new TableTransactionAction(TableTransactionActionType.UpsertReplace, entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
await _table.ExecuteBatchAsync(batch);
|
await _tableClient.SubmitTransactionAsync(batch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(InstallationDeviceEntity entity)
|
public async Task DeleteAsync(InstallationDeviceEntity entity)
|
||||||
{
|
{
|
||||||
try
|
await _tableClient.DeleteEntityAsync(entity.PartitionKey, entity.RowKey);
|
||||||
{
|
|
||||||
entity.ETag = "*";
|
|
||||||
await _table.ExecuteAsync(TableOperation.Delete(entity));
|
|
||||||
}
|
|
||||||
catch (StorageException e) when (e.RequestInformation.HttpStatusCode != (int)HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
@ -28,6 +29,12 @@ public interface IPaymentService
|
|||||||
int newlyPurchasedAdditionalStorage,
|
int newlyPurchasedAdditionalStorage,
|
||||||
DateTime? prorationDate = null);
|
DateTime? prorationDate = null);
|
||||||
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||||
|
Task<string> AdjustSeats(
|
||||||
|
Provider provider,
|
||||||
|
Plan plan,
|
||||||
|
int currentlySubscribedSeats,
|
||||||
|
int newlySubscribedSeats,
|
||||||
|
DateTime? prorationDate = null);
|
||||||
Task<string> AdjustSmSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
Task<string> AdjustSmSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||||
Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
|
Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ public interface IPushRegistrationService
|
|||||||
{
|
{
|
||||||
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||||
string identifier, DeviceType type);
|
string identifier, DeviceType type);
|
||||||
Task DeleteRegistrationAsync(string deviceId);
|
Task DeleteRegistrationAsync(string deviceId, DeviceType type);
|
||||||
Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
||||||
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Models.BitStripe;
|
using Bit.Core.Models.BitStripe;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ public interface IStripeAdapter
|
|||||||
Task<Stripe.Invoice> InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options);
|
Task<Stripe.Invoice> InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options);
|
||||||
Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options);
|
Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options);
|
||||||
Task<List<Stripe.Invoice>> InvoiceListAsync(StripeInvoiceListOptions options);
|
Task<List<Stripe.Invoice>> InvoiceListAsync(StripeInvoiceListOptions options);
|
||||||
|
Task<List<Stripe.Invoice>> InvoiceSearchAsync(InvoiceSearchOptions options);
|
||||||
Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options);
|
Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options);
|
||||||
Task<Stripe.Invoice> InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options);
|
Task<Stripe.Invoice> InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options);
|
||||||
Task<Stripe.Invoice> InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options);
|
Task<Stripe.Invoice> InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options);
|
||||||
|
@ -38,13 +38,13 @@ public class DeviceService : IDeviceService
|
|||||||
public async Task ClearTokenAsync(Device device)
|
public async Task ClearTokenAsync(Device device)
|
||||||
{
|
{
|
||||||
await _deviceRepository.ClearPushTokenAsync(device.Id);
|
await _deviceRepository.ClearPushTokenAsync(device.Id);
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(Device device)
|
public async Task DeleteAsync(Device device)
|
||||||
{
|
{
|
||||||
await _deviceRepository.DeleteAsync(device);
|
await _deviceRepository.DeleteAsync(device);
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||||
|
@ -951,7 +951,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||||
IEnumerable<string> ownerEmails)
|
IEnumerable<string> ownerEmails)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Service Accounts Limit Reached", ownerEmails);
|
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Machine Accounts Limit Reached", ownerEmails);
|
||||||
var model = new OrganizationServiceAccountsMaxReachedViewModel
|
var model = new OrganizationServiceAccountsMaxReachedViewModel
|
||||||
{
|
{
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
|
@ -43,7 +43,8 @@ public class MultiServicePushNotificationService : IPushNotificationService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.NotificationHub.ConnectionString))
|
var generalHub = globalSettings.NotificationHubs?.FirstOrDefault(h => h.HubType == NotificationHubType.General);
|
||||||
|
if (CoreHelpers.SettingHasValue(generalHub?.ConnectionString))
|
||||||
{
|
{
|
||||||
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
|
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
|
||||||
globalSettings, httpContextAccessor, hubLogger));
|
globalSettings, httpContextAccessor, hubLogger));
|
||||||
|
@ -20,8 +20,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
|||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private NotificationHubClient _client = null;
|
private readonly List<NotificationHubClient> _clients = [];
|
||||||
private ILogger _logger;
|
private readonly bool _enableTracing = false;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public NotificationHubPushNotificationService(
|
public NotificationHubPushNotificationService(
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
IInstallationDeviceRepository installationDeviceRepository,
|
||||||
@ -32,10 +33,18 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
|||||||
_installationDeviceRepository = installationDeviceRepository;
|
_installationDeviceRepository = installationDeviceRepository;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_client = NotificationHubClient.CreateClientFromConnectionString(
|
|
||||||
_globalSettings.NotificationHub.ConnectionString,
|
foreach (var hub in globalSettings.NotificationHubs)
|
||||||
_globalSettings.NotificationHub.HubName,
|
{
|
||||||
_globalSettings.NotificationHub.EnableSendTracing);
|
var client = NotificationHubClient.CreateClientFromConnectionString(
|
||||||
|
hub.ConnectionString,
|
||||||
|
hub.HubName,
|
||||||
|
hub.EnableSendTracing);
|
||||||
|
_clients.Add(client);
|
||||||
|
|
||||||
|
_enableTracing = _enableTracing || hub.EnableSendTracing;
|
||||||
|
}
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,16 +264,31 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
|||||||
|
|
||||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
||||||
{
|
{
|
||||||
var outcome = await _client.SendTemplateNotificationAsync(
|
var tasks = new List<Task<NotificationOutcome>>();
|
||||||
new Dictionary<string, string>
|
foreach (var client in _clients)
|
||||||
{
|
|
||||||
{ "type", ((byte)type).ToString() },
|
|
||||||
{ "payload", JsonSerializer.Serialize(payload) }
|
|
||||||
}, tag);
|
|
||||||
if (_globalSettings.NotificationHub.EnableSendTracing)
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
|
var task = client.SendTemplateNotificationAsync(
|
||||||
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "type", ((byte)type).ToString() },
|
||||||
|
{ "payload", JsonSerializer.Serialize(payload) }
|
||||||
|
}, tag);
|
||||||
|
tasks.Add(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
if (_enableTracing)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < tasks.Count; i++)
|
||||||
|
{
|
||||||
|
if (_clients[i].EnableTestSend)
|
||||||
|
{
|
||||||
|
var outcome = await tasks[i];
|
||||||
|
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
|
||||||
|
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Core.Models.Data;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Azure.NotificationHubs;
|
using Microsoft.Azure.NotificationHubs;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
@ -10,18 +11,36 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
{
|
{
|
||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||||
private NotificationHubClient _client = null;
|
private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
|
||||||
|
|
||||||
public NotificationHubPushRegistrationService(
|
public NotificationHubPushRegistrationService(
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
IInstallationDeviceRepository installationDeviceRepository,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings,
|
||||||
|
ILogger<NotificationHubPushRegistrationService> logger)
|
||||||
{
|
{
|
||||||
_installationDeviceRepository = installationDeviceRepository;
|
_installationDeviceRepository = installationDeviceRepository;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_client = NotificationHubClient.CreateClientFromConnectionString(
|
_logger = logger;
|
||||||
_globalSettings.NotificationHub.ConnectionString,
|
|
||||||
_globalSettings.NotificationHub.HubName);
|
// Is this dirty to do in the ctor?
|
||||||
|
void addHub(NotificationHubType type)
|
||||||
|
{
|
||||||
|
var hubRegistration = globalSettings.NotificationHubs.FirstOrDefault(
|
||||||
|
h => h.HubType == type && h.EnableRegistration);
|
||||||
|
if (hubRegistration != null)
|
||||||
|
{
|
||||||
|
var client = NotificationHubClient.CreateClientFromConnectionString(
|
||||||
|
hubRegistration.ConnectionString,
|
||||||
|
hubRegistration.HubName,
|
||||||
|
hubRegistration.EnableSendTracing);
|
||||||
|
_clients.Add(type, client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addHub(NotificationHubType.General);
|
||||||
|
addHub(NotificationHubType.iOS);
|
||||||
|
addHub(NotificationHubType.Android);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||||
@ -84,7 +103,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
||||||
userId, identifier);
|
userId, identifier);
|
||||||
|
|
||||||
await _client.CreateOrUpdateInstallationAsync(installation);
|
await GetClient(type).CreateOrUpdateInstallationAsync(installation);
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||||
{
|
{
|
||||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||||
@ -119,11 +138,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
installation.Templates.Add(fullTemplateId, template);
|
installation.Templates.Add(fullTemplateId, template);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteRegistrationAsync(string deviceId)
|
public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _client.DeleteInstallationAsync(deviceId);
|
await GetClient(deviceType).DeleteInstallationAsync(deviceId);
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||||
{
|
{
|
||||||
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
|
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
|
||||||
@ -135,31 +154,31 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||||
{
|
{
|
||||||
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
||||||
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
|
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
||||||
{
|
{
|
||||||
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
|
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
||||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||||
{
|
{
|
||||||
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove,
|
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Remove,
|
||||||
$"organizationId:{organizationId}");
|
$"organizationId:{organizationId}");
|
||||||
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
|
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
||||||
{
|
{
|
||||||
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
|
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
||||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,
|
private async Task PatchTagsForUserDevicesAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, UpdateOperationType op,
|
||||||
string tag)
|
string tag)
|
||||||
{
|
{
|
||||||
if (!deviceIds.Any())
|
if (!devices.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -179,11 +198,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
operation.Path += $"/{tag}";
|
operation.Path += $"/{tag}";
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var id in deviceIds)
|
foreach (var device in devices)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _client.PatchInstallationAsync(id, new List<PartialUpdateOperation> { operation });
|
await GetClient(device.Value).PatchInstallationAsync(device.Key, new List<PartialUpdateOperation> { operation });
|
||||||
}
|
}
|
||||||
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
||||||
{
|
{
|
||||||
@ -191,4 +210,54 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NotificationHubClient GetClient(DeviceType deviceType)
|
||||||
|
{
|
||||||
|
var hubType = NotificationHubType.General;
|
||||||
|
switch (deviceType)
|
||||||
|
{
|
||||||
|
case DeviceType.Android:
|
||||||
|
hubType = NotificationHubType.Android;
|
||||||
|
break;
|
||||||
|
case DeviceType.iOS:
|
||||||
|
hubType = NotificationHubType.iOS;
|
||||||
|
break;
|
||||||
|
case DeviceType.ChromeExtension:
|
||||||
|
case DeviceType.FirefoxExtension:
|
||||||
|
case DeviceType.OperaExtension:
|
||||||
|
case DeviceType.EdgeExtension:
|
||||||
|
case DeviceType.VivaldiExtension:
|
||||||
|
case DeviceType.SafariExtension:
|
||||||
|
hubType = NotificationHubType.GeneralBrowserExtension;
|
||||||
|
break;
|
||||||
|
case DeviceType.WindowsDesktop:
|
||||||
|
case DeviceType.MacOsDesktop:
|
||||||
|
case DeviceType.LinuxDesktop:
|
||||||
|
hubType = NotificationHubType.GeneralDesktop;
|
||||||
|
break;
|
||||||
|
case DeviceType.ChromeBrowser:
|
||||||
|
case DeviceType.FirefoxBrowser:
|
||||||
|
case DeviceType.OperaBrowser:
|
||||||
|
case DeviceType.EdgeBrowser:
|
||||||
|
case DeviceType.IEBrowser:
|
||||||
|
case DeviceType.UnknownBrowser:
|
||||||
|
case DeviceType.SafariBrowser:
|
||||||
|
case DeviceType.VivaldiBrowser:
|
||||||
|
hubType = NotificationHubType.GeneralWeb;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_clients.ContainsKey(hubType))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No hub client for '{0}'. Using general hub instead.", hubType);
|
||||||
|
hubType = NotificationHubType.General;
|
||||||
|
if (!_clients.ContainsKey(hubType))
|
||||||
|
{
|
||||||
|
throw new Exception("No general hub client found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _clients[hubType];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,30 +38,37 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
|
|||||||
await SendAsync(HttpMethod.Post, "push/register", requestModel);
|
await SendAsync(HttpMethod.Post, "push/register", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteRegistrationAsync(string deviceId)
|
public async Task DeleteRegistrationAsync(string deviceId, DeviceType type)
|
||||||
{
|
{
|
||||||
await SendAsync(HttpMethod.Delete, string.Concat("push/", deviceId));
|
var requestModel = new PushDeviceRequestModel
|
||||||
|
{
|
||||||
|
Id = deviceId,
|
||||||
|
Type = type,
|
||||||
|
};
|
||||||
|
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
public async Task AddUserRegistrationOrganizationAsync(
|
||||||
|
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||||
{
|
{
|
||||||
if (!deviceIds.Any())
|
if (!devices.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
||||||
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
public async Task DeleteUserRegistrationOrganizationAsync(
|
||||||
|
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||||
{
|
{
|
||||||
if (!deviceIds.Any())
|
if (!devices.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
||||||
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Models.BitStripe;
|
using Bit.Core.Models.BitStripe;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
@ -103,6 +104,9 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
return invoices;
|
return invoices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<Stripe.Invoice>> InvoiceSearchAsync(InvoiceSearchOptions options)
|
||||||
|
=> (await _invoiceService.SearchAsync(options)).Data;
|
||||||
|
|
||||||
public Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options)
|
public Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options)
|
||||||
{
|
{
|
||||||
return _invoiceService.UpdateAsync(id, options);
|
return _invoiceService.UpdateAsync(id, options);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -757,14 +758,14 @@ public class StripePaymentService : IPaymentService
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> FinalizeSubscriptionChangeAsync(IStorableSubscriber storableSubscriber,
|
private async Task<string> FinalizeSubscriptionChangeAsync(ISubscriber subscriber,
|
||||||
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false)
|
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false)
|
||||||
{
|
{
|
||||||
// remember, when in doubt, throw
|
// remember, when in doubt, throw
|
||||||
var subGetOptions = new SubscriptionGetOptions();
|
var subGetOptions = new SubscriptionGetOptions();
|
||||||
// subGetOptions.AddExpand("customer");
|
// subGetOptions.AddExpand("customer");
|
||||||
subGetOptions.AddExpand("customer.tax");
|
subGetOptions.AddExpand("customer.tax");
|
||||||
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId, subGetOptions);
|
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subGetOptions);
|
||||||
if (sub == null)
|
if (sub == null)
|
||||||
{
|
{
|
||||||
throw new GatewayException("Subscription not found.");
|
throw new GatewayException("Subscription not found.");
|
||||||
@ -776,6 +777,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
var chargeNow = collectionMethod == "charge_automatically";
|
var chargeNow = collectionMethod == "charge_automatically";
|
||||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||||
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
||||||
|
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
||||||
|
|
||||||
var subUpdateOptions = new SubscriptionUpdateOptions
|
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
@ -787,25 +789,10 @@ public class StripePaymentService : IPaymentService
|
|||||||
CollectionMethod = "send_invoice",
|
CollectionMethod = "send_invoice",
|
||||||
ProrationDate = prorationDate,
|
ProrationDate = prorationDate,
|
||||||
};
|
};
|
||||||
var immediatelyInvoice = false;
|
if (!invoiceNow && isAnnualPlan && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
|
||||||
if (!invoiceNow && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
|
|
||||||
{
|
{
|
||||||
var upcomingInvoiceWithChanges = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
subUpdateOptions.PendingInvoiceItemInterval =
|
||||||
{
|
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
|
||||||
Customer = storableSubscriber.GatewayCustomerId,
|
|
||||||
Subscription = storableSubscriber.GatewaySubscriptionId,
|
|
||||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(updatedItemOptions),
|
|
||||||
SubscriptionProrationBehavior = Constants.CreateProrations,
|
|
||||||
SubscriptionProrationDate = prorationDate,
|
|
||||||
SubscriptionBillingCycleAnchor = SubscriptionBillingCycleAnchor.Now
|
|
||||||
});
|
|
||||||
|
|
||||||
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
|
||||||
immediatelyInvoice = isAnnualPlan && upcomingInvoiceWithChanges.AmountRemaining >= 50000;
|
|
||||||
|
|
||||||
subUpdateOptions.BillingCycleAnchor = immediatelyInvoice
|
|
||||||
? SubscriptionBillingCycleAnchor.Now
|
|
||||||
: SubscriptionBillingCycleAnchor.Unchanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||||
@ -858,21 +845,16 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!isPm5864DollarThresholdEnabled || immediatelyInvoice || invoiceNow)
|
if (chargeNow)
|
||||||
{
|
{
|
||||||
if (chargeNow)
|
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(subscriber, invoice);
|
||||||
{
|
}
|
||||||
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(storableSubscriber, invoice);
|
else
|
||||||
}
|
{
|
||||||
else
|
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId,
|
||||||
{
|
new InvoiceFinalizeOptions { AutoAdvance = false, });
|
||||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new InvoiceFinalizeOptions
|
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new InvoiceSendOptions());
|
||||||
{
|
paymentIntentClientSecret = null;
|
||||||
AutoAdvance = false,
|
|
||||||
});
|
|
||||||
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new InvoiceSendOptions());
|
|
||||||
paymentIntentClientSecret = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@ -943,6 +925,17 @@ public class StripePaymentService : IPaymentService
|
|||||||
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<string> AdjustSeats(
|
||||||
|
Provider provider,
|
||||||
|
StaticStore.Plan plan,
|
||||||
|
int currentlySubscribedSeats,
|
||||||
|
int newlySubscribedSeats,
|
||||||
|
DateTime? prorationDate = null)
|
||||||
|
=> FinalizeSubscriptionChangeAsync(
|
||||||
|
provider,
|
||||||
|
new ProviderSubscriptionUpdate(plan.Type, currentlySubscribedSeats, newlySubscribedSeats),
|
||||||
|
prorationDate);
|
||||||
|
|
||||||
public Task<string> AdjustSmSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null)
|
public Task<string> AdjustSmSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null)
|
||||||
{
|
{
|
||||||
return FinalizeSubscriptionChangeAsync(organization, new SmSeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
return FinalizeSubscriptionChangeAsync(organization, new SmSeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
||||||
@ -1610,10 +1603,25 @@ public class StripePaymentService : IPaymentService
|
|||||||
return subscriptionInfo;
|
return subscriptionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
|
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, new SubscriptionGetOptions
|
||||||
|
{
|
||||||
|
Expand = ["test_clock"]
|
||||||
|
});
|
||||||
|
|
||||||
if (sub != null)
|
if (sub != null)
|
||||||
{
|
{
|
||||||
subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub);
|
subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub);
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.AC1795_UpdatedSubscriptionStatusSection))
|
||||||
|
{
|
||||||
|
var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub);
|
||||||
|
|
||||||
|
if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)
|
||||||
|
{
|
||||||
|
subscriptionInfo.Subscription.SuspensionDate = suspensionDate;
|
||||||
|
subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
if (sub is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||||
@ -1930,4 +1938,45 @@ public class StripePaymentService : IPaymentService
|
|||||||
? subscriberName
|
? subscriberName
|
||||||
: subscriberName[..30];
|
: subscriberName[..30];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
|
||||||
|
{
|
||||||
|
if (subscription.Status is not "past_due" && subscription.Status is not "unpaid")
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var openInvoices = await _stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions
|
||||||
|
{
|
||||||
|
Query = $"subscription:'{subscription.Id}' status:'open'"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (openInvoices.Count == 0)
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentDate = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
|
||||||
|
|
||||||
|
switch (subscription.CollectionMethod)
|
||||||
|
{
|
||||||
|
case "charge_automatically":
|
||||||
|
{
|
||||||
|
var firstOverdueInvoice = openInvoices
|
||||||
|
.Where(invoice => invoice.PeriodEnd < currentDate && invoice.Attempted)
|
||||||
|
.MinBy(invoice => invoice.Created);
|
||||||
|
|
||||||
|
return (firstOverdueInvoice?.Created.AddDays(14), firstOverdueInvoice?.PeriodEnd);
|
||||||
|
}
|
||||||
|
case "send_invoice":
|
||||||
|
{
|
||||||
|
var firstOverdueInvoice = openInvoices
|
||||||
|
.Where(invoice => invoice.DueDate < currentDate)
|
||||||
|
.MinBy(invoice => invoice.Created);
|
||||||
|
|
||||||
|
return (firstOverdueInvoice?.DueDate?.AddDays(30), firstOverdueInvoice?.PeriodEnd);
|
||||||
|
}
|
||||||
|
default: return (null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user