diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 4600e62d27..5a1491f7c7 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -395,35 +395,9 @@ namespace Bit.Api.Controllers var valid = model.Validate(_globalSettings); UserLicense license = null; - if(valid && _globalSettings.SelfHosted && model.License != null) + if(valid && _globalSettings.SelfHosted) { - if(!HttpContext.Request.ContentLength.HasValue || HttpContext.Request.ContentLength.Value > 51200) // 50 KB - { - valid = false; - } - else - { - try - { - using(var stream = model.License.OpenReadStream()) - using(var reader = new StreamReader(stream)) - { - var s = await reader.ReadToEndAsync(); - if(string.IsNullOrWhiteSpace(s)) - { - valid = false; - } - else - { - license = JsonConvert.DeserializeObject(s); - } - } - } - catch - { - valid = false; - } - } + license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); } if(!valid || (_globalSettings.SelfHosted && license == null)) @@ -488,7 +462,7 @@ namespace Bit.Api.Controllers [HttpPut("license")] [HttpPost("license")] [SelfHosted(SelfHostedOnly = true)] - public async Task PutLicense(UpdateLicenseRequestModel model) + public async Task PutLicense(LicenseRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if(user == null) @@ -496,24 +470,7 @@ namespace Bit.Api.Controllers throw new UnauthorizedAccessException(); } - UserLicense license = null; - if(HttpContext.Request.ContentLength.HasValue && HttpContext.Request.ContentLength.Value <= 51200) // 50 KB - { - try - { - using(var stream = model.License.OpenReadStream()) - using(var reader = new StreamReader(stream)) - { - var s = await reader.ReadToEndAsync(); - if(!string.IsNullOrWhiteSpace(s)) - { - license = JsonConvert.DeserializeObject(s); - } - } - } - catch { } - } - + var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); if(license == null) { throw new BadRequestException("Invalid license"); diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 3eee6a5839..81facbc9c9 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -11,6 +11,8 @@ using Bit.Core; using Microsoft.AspNetCore.Identity; using Bit.Core.Models.Table; using Bit.Core.Utilities; +using Bit.Api.Utilities; +using Bit.Core.Models.Business; namespace Bit.Api.Controllers { @@ -94,6 +96,7 @@ namespace Bit.Api.Controllers } [HttpPost("")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task Post([FromBody]OrganizationCreateRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -107,6 +110,26 @@ namespace Bit.Api.Controllers return new OrganizationResponseModel(result.Item1); } + [HttpPost("license")] + [SelfHosted(SelfHostedOnly = true)] + public async Task PostLicense(OrganizationCreateLicenseRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if(user == null) + { + throw new UnauthorizedAccessException(); + } + + var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); + if(license == null) + { + throw new BadRequestException("Invalid license"); + } + + var result = await _organizationService.SignUpAsync(license, user, model.Key); + return new OrganizationResponseModel(result.Item1); + } + [HttpPut("{id}")] [HttpPost("{id}")] public async Task Put(string id, [FromBody]OrganizationUpdateRequestModel model) @@ -132,6 +155,7 @@ namespace Bit.Api.Controllers [HttpPut("{id}/payment")] [HttpPost("{id}/payment")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PutPayment(string id, [FromBody]PaymentRequestModel model) { var orgIdGuid = new Guid(id); @@ -145,6 +169,7 @@ namespace Bit.Api.Controllers [HttpPut("{id}/upgrade")] [HttpPost("{id}/upgrade")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PutUpgrade(string id, [FromBody]OrganizationUpgradeRequestModel model) { var orgIdGuid = new Guid(id); @@ -158,6 +183,7 @@ namespace Bit.Api.Controllers [HttpPut("{id}/seat")] [HttpPost("{id}/seat")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PutSeat(string id, [FromBody]OrganizationSeatRequestModel model) { var orgIdGuid = new Guid(id); @@ -171,6 +197,7 @@ namespace Bit.Api.Controllers [HttpPut("{id}/storage")] [HttpPost("{id}/storage")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PutStorage(string id, [FromBody]StorageRequestModel model) { var orgIdGuid = new Guid(id); @@ -183,6 +210,7 @@ namespace Bit.Api.Controllers } [HttpPost("{id}/verify-bank")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PostVerifyBank(string id, [FromBody]OrganizationVerifyBankRequestModel model) { var orgIdGuid = new Guid(id); @@ -196,6 +224,7 @@ namespace Bit.Api.Controllers [HttpPut("{id}/cancel")] [HttpPost("{id}/cancel")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PutCancel(string id) { var orgIdGuid = new Guid(id); @@ -209,6 +238,7 @@ namespace Bit.Api.Controllers [HttpPut("{id}/reinstate")] [HttpPost("{id}/reinstate")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PutReinstate(string id) { var orgIdGuid = new Guid(id); diff --git a/src/Api/Utilities/ApiHelpers.cs b/src/Api/Utilities/ApiHelpers.cs new file mode 100644 index 0000000000..71586fdf20 --- /dev/null +++ b/src/Api/Utilities/ApiHelpers.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System.IO; +using System.Threading.Tasks; + +namespace Bit.Api.Utilities +{ + public static class ApiHelpers + { + public async static Task ReadJsonFileFromBody(HttpContext httpContext, IFormFile file, long maxSize = 51200) + { + T obj = default(T); + if(file != null && httpContext.Request.ContentLength.HasValue && httpContext.Request.ContentLength.Value <= maxSize) + { + try + { + using(var stream = file.OpenReadStream()) + using(var reader = new StreamReader(stream)) + { + var s = await reader.ReadToEndAsync(); + if(!string.IsNullOrWhiteSpace(s)) + { + obj = JsonConvert.DeserializeObject(s); + } + } + } + catch { } + } + + return obj; + } + } +} diff --git a/src/Core/GlobalSettings.cs b/src/Core/GlobalSettings.cs index c6ee88aca4..762575c3fc 100644 --- a/src/Core/GlobalSettings.cs +++ b/src/Core/GlobalSettings.cs @@ -111,7 +111,7 @@ namespace Bit.Core public class InstallationSettings { - public Guid? Id { get; set; } + public Guid Id { get; set; } public string Key { get; set; } public string IdentityUri { get; set; } } diff --git a/src/Core/Models/Api/Request/Accounts/UpdateLicenseRequestModel.cs b/src/Core/Models/Api/Request/LicenseRequestModel.cs similarity index 81% rename from src/Core/Models/Api/Request/Accounts/UpdateLicenseRequestModel.cs rename to src/Core/Models/Api/Request/LicenseRequestModel.cs index 5eb2a53b43..bbd1393a12 100644 --- a/src/Core/Models/Api/Request/Accounts/UpdateLicenseRequestModel.cs +++ b/src/Core/Models/Api/Request/LicenseRequestModel.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Bit.Core.Models.Api { - public class UpdateLicenseRequestModel + public class LicenseRequestModel { [Required] public IFormFile License { get; set; } diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationCreateLicenseRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationCreateLicenseRequestModel.cs new file mode 100644 index 0000000000..edcc212dbc --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationCreateLicenseRequestModel.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class OrganizationCreateLicenseRequestModel : LicenseRequestModel + { + [Required] + public string Key { get; set; } + } +} diff --git a/src/Core/Models/Business/OrganizationSignup.cs b/src/Core/Models/Business/OrganizationSignup.cs index 3a1c8e424f..76e0136bf6 100644 --- a/src/Core/Models/Business/OrganizationSignup.cs +++ b/src/Core/Models/Business/OrganizationSignup.cs @@ -1,5 +1,4 @@ using Bit.Core.Models.Table; -using System; namespace Bit.Core.Models.Business { diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 38ce44fbe9..aede51690a 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -18,6 +18,7 @@ namespace Bit.Core.Services Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); Task> SignUpAsync(OrganizationSignup organizationSignup); + Task> SignUpAsync(OrganizationLicense license, User owner, string ownerKey); Task DeleteAsync(Organization organization); Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 2e07006eb3..ac2f9b099a 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -26,7 +26,9 @@ namespace Bit.Core.Services private readonly IPushNotificationService _pushNotificationService; private readonly IPushRegistrationService _pushRegistrationService; private readonly IDeviceRepository _deviceRepository; + private readonly ILicensingService _licensingService; private readonly StripePaymentService _stripePaymentService; + private readonly GlobalSettings _globalSettings; public OrganizationService( IOrganizationRepository organizationRepository, @@ -38,7 +40,9 @@ namespace Bit.Core.Services IMailService mailService, IPushNotificationService pushNotificationService, IPushRegistrationService pushRegistrationService, - IDeviceRepository deviceRepository) + IDeviceRepository deviceRepository, + ILicensingService licensingService, + GlobalSettings globalSettings) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -50,7 +54,9 @@ namespace Bit.Core.Services _pushNotificationService = pushNotificationService; _pushRegistrationService = pushRegistrationService; _deviceRepository = deviceRepository; + _licensingService = licensingService; _stripePaymentService = new StripePaymentService(); + _globalSettings = globalSettings; } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken) @@ -401,11 +407,6 @@ namespace Bit.Core.Services throw new BadRequestException("Plan not found."); } - var customerService = new StripeCustomerService(); - var subscriptionService = new StripeSubscriptionService(); - StripeCustomer customer = null; - StripeSubscription subscription = null; - if(!plan.MaxStorageGb.HasValue && signup.AdditionalStorageGb > 0) { throw new BadRequestException("Plan does not allow additional storage."); @@ -428,6 +429,11 @@ namespace Bit.Core.Services $"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users."); } + var customerService = new StripeCustomerService(); + var subscriptionService = new StripeSubscriptionService(); + StripeCustomer customer = null; + StripeSubscription subscription = null; + // Pre-generate the org id so that we can save it with the Stripe subscription.. Guid newOrgId = CoreHelpers.GenerateComb(); @@ -525,6 +531,52 @@ namespace Bit.Core.Services RevisionDate = DateTime.UtcNow }; + return await SignUpAsync(organization, signup.Owner.Id, signup.OwnerKey, true); + } + + public async Task> SignUpAsync( + OrganizationLicense license, User owner, string ownerKey) + { + if(license == null || !_licensingService.VerifyLicense(license) || !license.CanUse(_globalSettings.Installation.Id)) + { + throw new BadRequestException("Invalid license."); + } + + var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled); + if(plan == null) + { + throw new BadRequestException("Plan not found."); + } + + var organization = new Organization + { + Name = license.Name, + BillingEmail = null, + BusinessName = null, + PlanType = license.PlanType, + Seats = license.Seats, + MaxCollections = license.MaxCollections, + MaxStorageGb = 10240, // 10 TB + UseGroups = license.UseGroups, + UseDirectory = license.UseDirectory, + UseTotp = license.UseTotp, + Plan = license.Plan, + Gateway = null, + GatewayCustomerId = null, + GatewaySubscriptionId = null, + Enabled = true, + ExpirationDate = license.Expires, + LicenseKey = license.LicenseKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow + }; + + return await SignUpAsync(organization, owner.Id, ownerKey, false); + } + + private async Task> SignUpAsync(Organization organization, + Guid ownerId, string ownerKey, bool withPayment) + { try { await _organizationRepository.CreateAsync(organization); @@ -532,8 +584,8 @@ namespace Bit.Core.Services var orgUser = new OrganizationUser { OrganizationId = organization.Id, - UserId = signup.Owner.Id, - Key = signup.OwnerKey, + UserId = ownerId, + Key = ownerKey, Type = OrganizationUserType.Owner, Status = OrganizationUserStatusType.Confirmed, AccessAll = true, @@ -546,13 +598,17 @@ namespace Bit.Core.Services // push var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value); await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds, organization.Id.ToString()); - await _pushNotificationService.PushSyncOrgKeysAsync(signup.Owner.Id); + await _pushNotificationService.PushSyncOrgKeysAsync(ownerId); return new Tuple(organization, orgUser); } catch { - await _stripePaymentService.CancelAndRecoverChargesAsync(organization); + if(withPayment) + { + await _stripePaymentService.CancelAndRecoverChargesAsync(organization); + } + if(organization.Id != default(Guid)) { await _organizationRepository.DeleteAsync(organization);