2017-03-03 00:07:11 -05:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using Bit.Core.Repositories;
|
|
|
|
|
|
using Bit.Core.Models.Business;
|
2017-03-08 21:45:08 -05:00
|
|
|
|
using Bit.Core.Models.Table;
|
2017-03-03 00:07:11 -05:00
|
|
|
|
using Bit.Core.Utilities;
|
|
|
|
|
|
using Bit.Core.Exceptions;
|
2017-03-09 23:58:43 -05:00
|
|
|
|
using System.Collections.Generic;
|
2017-03-23 00:17:34 -04:00
|
|
|
|
using Microsoft.AspNetCore.DataProtection;
|
2017-04-04 10:13:16 -04:00
|
|
|
|
using Stripe;
|
2017-04-10 10:44:27 -04:00
|
|
|
|
using Bit.Core.Enums;
|
2017-05-11 14:52:35 -04:00
|
|
|
|
using Bit.Core.Models.Data;
|
2017-08-14 21:25:06 -04:00
|
|
|
|
using System.IO;
|
|
|
|
|
|
using Newtonsoft.Json;
|
2017-03-03 00:07:11 -05:00
|
|
|
|
|
|
|
|
|
|
namespace Bit.Core.Services
|
|
|
|
|
|
{
|
|
|
|
|
|
public class OrganizationService : IOrganizationService
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly IOrganizationRepository _organizationRepository;
|
|
|
|
|
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
2017-04-27 09:19:30 -04:00
|
|
|
|
private readonly ICollectionRepository _collectionRepository;
|
2017-03-04 21:28:41 -05:00
|
|
|
|
private readonly IUserRepository _userRepository;
|
2017-05-13 12:00:40 -04:00
|
|
|
|
private readonly IGroupRepository _groupRepository;
|
2017-03-23 00:17:34 -04:00
|
|
|
|
private readonly IDataProtector _dataProtector;
|
|
|
|
|
|
private readonly IMailService _mailService;
|
2017-05-26 22:52:50 -04:00
|
|
|
|
private readonly IPushNotificationService _pushNotificationService;
|
|
|
|
|
|
private readonly IPushRegistrationService _pushRegistrationService;
|
2017-08-11 08:57:31 -04:00
|
|
|
|
private readonly IDeviceRepository _deviceRepository;
|
2017-08-14 20:57:45 -04:00
|
|
|
|
private readonly ILicensingService _licensingService;
|
2017-12-01 16:00:30 -05:00
|
|
|
|
private readonly IEventService _eventService;
|
2017-08-15 16:11:08 -04:00
|
|
|
|
private readonly IInstallationRepository _installationRepository;
|
2017-12-19 16:02:39 -05:00
|
|
|
|
private readonly IApplicationCacheService _applicationCacheService;
|
2019-02-08 23:53:09 -05:00
|
|
|
|
private readonly IPaymentService _paymentService;
|
2020-01-15 15:00:54 -05:00
|
|
|
|
private readonly IPolicyRepository _policyRepository;
|
2020-07-22 09:38:39 -04:00
|
|
|
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
2020-08-26 14:12:04 -04:00
|
|
|
|
private readonly ISsoUserRepository _ssoUserRepository;
|
2020-07-07 12:01:34 -04:00
|
|
|
|
private readonly IReferenceEventService _referenceEventService;
|
2017-08-14 20:57:45 -04:00
|
|
|
|
private readonly GlobalSettings _globalSettings;
|
2017-03-03 00:07:11 -05:00
|
|
|
|
|
|
|
|
|
|
public OrganizationService(
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
2017-03-04 21:28:41 -05:00
|
|
|
|
IOrganizationUserRepository organizationUserRepository,
|
2017-04-27 09:19:30 -04:00
|
|
|
|
ICollectionRepository collectionRepository,
|
2017-03-23 00:17:34 -04:00
|
|
|
|
IUserRepository userRepository,
|
2017-05-13 12:00:40 -04:00
|
|
|
|
IGroupRepository groupRepository,
|
2017-03-23 00:17:34 -04:00
|
|
|
|
IDataProtectionProvider dataProtectionProvider,
|
2017-04-21 14:22:32 -04:00
|
|
|
|
IMailService mailService,
|
2017-05-26 22:52:50 -04:00
|
|
|
|
IPushNotificationService pushNotificationService,
|
2017-08-11 08:57:31 -04:00
|
|
|
|
IPushRegistrationService pushRegistrationService,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
IDeviceRepository deviceRepository,
|
|
|
|
|
|
ILicensingService licensingService,
|
2017-12-01 16:00:30 -05:00
|
|
|
|
IEventService eventService,
|
2017-08-15 16:11:08 -04:00
|
|
|
|
IInstallationRepository installationRepository,
|
2017-12-19 16:02:39 -05:00
|
|
|
|
IApplicationCacheService applicationCacheService,
|
2019-02-08 23:53:09 -05:00
|
|
|
|
IPaymentService paymentService,
|
2020-01-15 15:00:54 -05:00
|
|
|
|
IPolicyRepository policyRepository,
|
2020-07-22 09:38:39 -04:00
|
|
|
|
ISsoConfigRepository ssoConfigRepository,
|
2020-08-26 14:12:04 -04:00
|
|
|
|
ISsoUserRepository ssoUserRepository,
|
2020-07-07 12:01:34 -04:00
|
|
|
|
IReferenceEventService referenceEventService,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
GlobalSettings globalSettings)
|
2017-03-03 00:07:11 -05:00
|
|
|
|
{
|
|
|
|
|
|
_organizationRepository = organizationRepository;
|
|
|
|
|
|
_organizationUserRepository = organizationUserRepository;
|
2017-04-27 09:19:30 -04:00
|
|
|
|
_collectionRepository = collectionRepository;
|
2017-03-04 21:28:41 -05:00
|
|
|
|
_userRepository = userRepository;
|
2017-05-13 12:00:40 -04:00
|
|
|
|
_groupRepository = groupRepository;
|
2017-03-23 00:17:34 -04:00
|
|
|
|
_dataProtector = dataProtectionProvider.CreateProtector("OrganizationServiceDataProtector");
|
|
|
|
|
|
_mailService = mailService;
|
2017-05-26 22:52:50 -04:00
|
|
|
|
_pushNotificationService = pushNotificationService;
|
|
|
|
|
|
_pushRegistrationService = pushRegistrationService;
|
2017-08-11 08:57:31 -04:00
|
|
|
|
_deviceRepository = deviceRepository;
|
2017-08-14 20:57:45 -04:00
|
|
|
|
_licensingService = licensingService;
|
2017-12-01 16:00:30 -05:00
|
|
|
|
_eventService = eventService;
|
2017-08-15 16:11:08 -04:00
|
|
|
|
_installationRepository = installationRepository;
|
2017-12-19 16:02:39 -05:00
|
|
|
|
_applicationCacheService = applicationCacheService;
|
2019-02-08 23:53:09 -05:00
|
|
|
|
_paymentService = paymentService;
|
2020-01-15 15:00:54 -05:00
|
|
|
|
_policyRepository = policyRepository;
|
2020-07-22 09:38:39 -04:00
|
|
|
|
_ssoConfigRepository = ssoConfigRepository;
|
2020-08-26 14:12:04 -04:00
|
|
|
|
_ssoUserRepository = ssoUserRepository;
|
2020-07-07 12:01:34 -04:00
|
|
|
|
_referenceEventService = referenceEventService;
|
2017-08-14 20:57:45 -04:00
|
|
|
|
_globalSettings = globalSettings;
|
2017-03-03 00:07:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-02-19 17:13:21 -05:00
|
|
|
|
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
2020-06-17 19:49:27 -04:00
|
|
|
|
PaymentMethodType paymentMethodType, TaxInfo taxInfo)
|
2017-04-08 16:41:40 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-04-08 16:41:40 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-06-17 19:49:27 -04:00
|
|
|
|
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
|
2019-02-08 23:53:09 -05:00
|
|
|
|
var updated = await _paymentService.UpdatePaymentMethodAsync(organization,
|
2019-02-26 12:45:34 -05:00
|
|
|
|
paymentMethodType, paymentToken);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (updated)
|
2017-04-08 16:41:40 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await ReplaceAndUpdateCache(organization);
|
2017-04-08 16:41:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-12-31 13:34:02 -05:00
|
|
|
|
public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null)
|
2017-04-08 18:15:20 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-04-08 18:15:20 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-12-31 13:34:02 -05:00
|
|
|
|
var eop = endOfPeriod.GetValueOrDefault(true);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!endOfPeriod.HasValue && organization.ExpirationDate.HasValue &&
|
2018-12-31 13:34:02 -05:00
|
|
|
|
organization.ExpirationDate.Value < DateTime.UtcNow)
|
|
|
|
|
|
{
|
|
|
|
|
|
eop = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-02-08 23:53:09 -05:00
|
|
|
|
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
2020-07-07 12:01:34 -04:00
|
|
|
|
await _referenceEventService.RaiseEventAsync(
|
|
|
|
|
|
new ReferenceEvent(ReferenceEventType.CancelSubscription, organization)
|
|
|
|
|
|
{
|
|
|
|
|
|
EndOfPeriod = endOfPeriod,
|
|
|
|
|
|
});
|
2017-04-08 18:15:20 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-10 18:20:21 -04:00
|
|
|
|
public async Task ReinstateSubscriptionAsync(Guid organizationId)
|
2017-04-10 16:42:53 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-04-10 16:42:53 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-02-08 23:53:09 -05:00
|
|
|
|
await _paymentService.ReinstateSubscriptionAsync(organization);
|
2020-07-07 12:01:34 -04:00
|
|
|
|
await _referenceEventService.RaiseEventAsync(
|
|
|
|
|
|
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization));
|
2017-04-10 16:42:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-08-09 23:56:26 -04:00
|
|
|
|
public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2019-03-21 21:36:03 -04:00
|
|
|
|
throw new BadRequestException("Your account has no payment method available.");
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var existingPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (existingPlan == null)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Existing plan not found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-03-21 21:36:03 -04:00
|
|
|
|
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (newPlan == null)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Plan not found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (existingPlan.Type == newPlan.Type)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Organization is already on this plan.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (existingPlan.UpgradeSortOrder >= newPlan.UpgradeSortOrder)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You cannot upgrade to this plan.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (existingPlan.Type != PlanType.Free)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2019-03-21 21:36:03 -04:00
|
|
|
|
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-03-21 21:36:03 -04:00
|
|
|
|
ValidateOrganizationUpgradeParameters(newPlan, upgrade);
|
2017-04-10 09:36:21 -04:00
|
|
|
|
|
2019-03-21 21:36:03 -04:00
|
|
|
|
var newPlanSeats = (short)(newPlan.BaseSeats +
|
2020-08-11 14:19:56 -04:00
|
|
|
|
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (userCount > newPlanSeats)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2019-03-21 21:36:03 -04:00
|
|
|
|
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
|
|
|
|
|
|
$"Your new plan only has ({newPlanSeats}) seats. Remove some users.");
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (newPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
|
2019-03-21 21:36:03 -04:00
|
|
|
|
organization.MaxCollections.Value > newPlan.MaxCollections.Value))
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2017-04-27 09:19:30 -04:00
|
|
|
|
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (collectionCount > newPlan.MaxCollections.Value)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2017-04-27 09:19:30 -04:00
|
|
|
|
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
|
|
|
|
|
|
$"Your new plan allows for a maximum of ({newPlan.MaxCollections.Value}) collections. " +
|
|
|
|
|
|
"Remove some collections.");
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-11 14:19:56 -04:00
|
|
|
|
if (!newPlan.HasGroups && organization.UseGroups)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2019-03-21 21:36:03 -04:00
|
|
|
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (groups.Any())
|
2017-05-08 14:40:04 -04:00
|
|
|
|
{
|
2019-03-21 21:36:03 -04:00
|
|
|
|
throw new BadRequestException($"Your new plan does not allow the groups feature. " +
|
|
|
|
|
|
$"Remove your groups.");
|
2017-05-08 14:40:04 -04:00
|
|
|
|
}
|
2019-03-21 21:36:03 -04:00
|
|
|
|
}
|
2017-05-08 14:40:04 -04:00
|
|
|
|
|
2020-08-11 14:19:56 -04:00
|
|
|
|
if (!newPlan.HasPolicies && organization.UsePolicies)
|
2020-01-15 15:00:54 -05:00
|
|
|
|
{
|
|
|
|
|
|
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (policies.Any(p => p.Enabled))
|
2020-01-15 15:00:54 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Your new plan does not allow the policies feature. " +
|
|
|
|
|
|
$"Disable your policies.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-08-11 14:19:56 -04:00
|
|
|
|
|
|
|
|
|
|
if (!newPlan.HasSso && organization.UseSso)
|
2020-07-22 09:38:39 -04:00
|
|
|
|
{
|
|
|
|
|
|
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
|
|
|
|
|
|
if (ssoConfig != null && ssoConfig.Enabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Your new plan does not allow the SSO feature. " +
|
|
|
|
|
|
$"Disable your SSO configuration.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-01-15 15:00:54 -05:00
|
|
|
|
|
2019-03-21 21:36:03 -04:00
|
|
|
|
// TODO: Check storage?
|
2017-04-10 09:36:21 -04:00
|
|
|
|
|
2019-08-09 23:56:26 -04:00
|
|
|
|
string paymentIntentClientSecret = null;
|
|
|
|
|
|
var success = true;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
2019-03-21 21:36:03 -04:00
|
|
|
|
{
|
2019-08-09 23:56:26 -04:00
|
|
|
|
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, newPlan,
|
2020-12-04 12:05:16 -05:00
|
|
|
|
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo);
|
2019-08-09 23:56:26 -04:00
|
|
|
|
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2019-03-21 21:36:03 -04:00
|
|
|
|
// TODO: Update existing sub
|
|
|
|
|
|
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
organization.BusinessName = upgrade.BusinessName;
|
|
|
|
|
|
organization.PlanType = newPlan.Type;
|
|
|
|
|
|
organization.Seats = (short)(newPlan.BaseSeats + upgrade.AdditionalSeats);
|
|
|
|
|
|
organization.MaxCollections = newPlan.MaxCollections;
|
2020-08-11 14:19:56 -04:00
|
|
|
|
organization.UseGroups = newPlan.HasGroups;
|
|
|
|
|
|
organization.UseDirectory = newPlan.HasDirectory;
|
|
|
|
|
|
organization.UseEvents = newPlan.HasEvents;
|
|
|
|
|
|
organization.UseTotp = newPlan.HasTotp;
|
|
|
|
|
|
organization.Use2fa = newPlan.Has2fa;
|
|
|
|
|
|
organization.UseApi = newPlan.HasApi;
|
|
|
|
|
|
organization.SelfHost = newPlan.HasSelfHost;
|
|
|
|
|
|
organization.UsePolicies = newPlan.HasPolicies;
|
2020-09-18 14:10:30 -04:00
|
|
|
|
organization.MaxStorageGb = !newPlan.BaseStorageGb.HasValue ?
|
|
|
|
|
|
(short?)null : (short)(newPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
|
2020-08-11 14:19:56 -04:00
|
|
|
|
organization.UseGroups = newPlan.HasGroups;
|
|
|
|
|
|
organization.UseDirectory = newPlan.HasDirectory;
|
|
|
|
|
|
organization.UseEvents = newPlan.HasEvents;
|
|
|
|
|
|
organization.UseTotp = newPlan.HasTotp;
|
|
|
|
|
|
organization.Use2fa = newPlan.Has2fa;
|
|
|
|
|
|
organization.UseApi = newPlan.HasApi;
|
2020-10-07 15:03:47 -04:00
|
|
|
|
organization.UseSso = newPlan.HasSso;
|
2020-08-11 14:19:56 -04:00
|
|
|
|
organization.SelfHost = newPlan.HasSelfHost;
|
2019-03-21 21:36:03 -04:00
|
|
|
|
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
|
|
|
|
|
|
organization.Plan = newPlan.Name;
|
2019-08-09 23:56:26 -04:00
|
|
|
|
organization.Enabled = success;
|
2019-03-21 21:36:03 -04:00
|
|
|
|
await ReplaceAndUpdateCache(organization);
|
2020-07-07 12:01:34 -04:00
|
|
|
|
if (success)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _referenceEventService.RaiseEventAsync(
|
|
|
|
|
|
new ReferenceEvent(ReferenceEventType.UpgradePlan, organization)
|
|
|
|
|
|
{
|
|
|
|
|
|
PlanName = newPlan.Name,
|
|
|
|
|
|
PlanType = newPlan.Type,
|
|
|
|
|
|
Seats = organization.Seats,
|
|
|
|
|
|
Storage = organization.MaxStorageGb,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2019-08-09 23:56:26 -04:00
|
|
|
|
|
|
|
|
|
|
return new Tuple<bool, string>(success, paymentIntentClientSecret);
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-08-10 12:59:32 -04:00
|
|
|
|
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
2017-07-11 10:59:59 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-07-11 10:59:59 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (plan == null)
|
2017-07-11 10:59:59 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Existing plan not found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-11 14:19:56 -04:00
|
|
|
|
if (!plan.HasAdditionalStorageOption)
|
2017-07-11 10:59:59 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Plan does not allow additional storage.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-08-10 12:59:32 -04:00
|
|
|
|
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
|
2018-11-20 22:02:09 -05:00
|
|
|
|
plan.StripeStoragePlanId);
|
2020-07-07 12:01:34 -04:00
|
|
|
|
await _referenceEventService.RaiseEventAsync(
|
|
|
|
|
|
new ReferenceEvent(ReferenceEventType.AdjustStorage, organization)
|
|
|
|
|
|
{
|
|
|
|
|
|
PlanName = plan.Name,
|
|
|
|
|
|
PlanType = plan.Type,
|
|
|
|
|
|
Storage = storageAdjustmentGb,
|
|
|
|
|
|
});
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await ReplaceAndUpdateCache(organization);
|
2019-08-10 12:59:32 -04:00
|
|
|
|
return secret;
|
2017-07-11 10:59:59 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-08-12 10:03:50 -04:00
|
|
|
|
public async Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("No payment method found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("No subscription found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (plan == null)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Existing plan not found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-11 14:19:56 -04:00
|
|
|
|
if (!plan.HasAdditionalSeatsOption)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2017-04-10 10:44:27 -04:00
|
|
|
|
throw new BadRequestException("Plan does not allow additional seats.");
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-10 11:49:53 -04:00
|
|
|
|
var newSeatTotal = organization.Seats + seatAdjustment;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (plan.BaseSeats > newSeatTotal)
|
2017-04-10 11:49:53 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Plan has a minimum of {plan.BaseSeats} seats.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (newSeatTotal <= 0)
|
2017-05-20 15:33:17 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You must have at least 1 seat.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-10 11:49:53 -04:00
|
|
|
|
var additionalSeats = newSeatTotal - plan.BaseSeats;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (plan.MaxAdditionalSeats.HasValue && additionalSeats > plan.MaxAdditionalSeats.Value)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Organization plan allows a maximum of " +
|
2017-04-10 10:44:27 -04:00
|
|
|
|
$"{plan.MaxAdditionalSeats.Value} additional seats.");
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (userCount > newSeatTotal)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2019-05-14 11:16:30 -04:00
|
|
|
|
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
|
|
|
|
|
|
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
|
2017-04-10 09:36:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-01-29 14:41:37 -05:00
|
|
|
|
var subscriptionItemService = new SubscriptionItemService();
|
|
|
|
|
|
var subscriptionService = new SubscriptionService();
|
2017-07-28 14:24:07 -04:00
|
|
|
|
var sub = await subscriptionService.GetAsync(organization.GatewaySubscriptionId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (sub == null)
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2017-04-10 12:28:41 -04:00
|
|
|
|
throw new BadRequestException("Subscription not found.");
|
|
|
|
|
|
}
|
2019-01-31 12:11:30 -05:00
|
|
|
|
|
2020-05-12 12:48:21 -04:00
|
|
|
|
var prorationDate = DateTime.UtcNow;
|
2017-04-10 12:28:41 -04:00
|
|
|
|
var seatItem = sub.Items?.Data?.FirstOrDefault(i => i.Plan.Id == plan.StripeSeatPlanId);
|
2020-06-30 11:52:50 -04:00
|
|
|
|
// Retain original collection method
|
|
|
|
|
|
var collectionMethod = sub.CollectionMethod;
|
2019-01-31 00:41:13 -05:00
|
|
|
|
|
2020-05-13 09:54:10 -04:00
|
|
|
|
var subResponse = await subscriptionService.UpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2020-05-12 17:22:04 -04:00
|
|
|
|
Items = new List<SubscriptionItemOptions>
|
2017-04-10 09:36:21 -04:00
|
|
|
|
{
|
2020-05-12 17:22:04 -04:00
|
|
|
|
new SubscriptionItemOptions
|
2019-01-31 00:41:13 -05:00
|
|
|
|
{
|
2020-05-12 12:48:21 -04:00
|
|
|
|
Id = seatItem?.Id,
|
2020-05-08 10:15:48 -04:00
|
|
|
|
Plan = plan.StripeSeatPlanId,
|
2019-01-31 00:41:13 -05:00
|
|
|
|
Quantity = additionalSeats,
|
2020-05-12 12:48:21 -04:00
|
|
|
|
Deleted = (seatItem?.Id != null && additionalSeats == 0) ? true : (bool?)null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
ProrationBehavior = "always_invoice",
|
|
|
|
|
|
DaysUntilDue = 1,
|
|
|
|
|
|
CollectionMethod = "send_invoice",
|
|
|
|
|
|
ProrationDate = prorationDate,
|
|
|
|
|
|
});
|
2017-04-10 09:36:21 -04:00
|
|
|
|
|
2019-08-12 10:03:50 -04:00
|
|
|
|
string paymentIntentClientSecret = null;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (additionalSeats > 0)
|
2017-04-11 10:00:36 -04:00
|
|
|
|
{
|
2020-05-12 12:48:21 -04:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
paymentIntentClientSecret = await (_paymentService as StripePaymentService)
|
2020-05-12 17:22:04 -04:00
|
|
|
|
.PayInvoiceAfterSubscriptionChangeAsync(organization, subResponse.LatestInvoiceId);
|
2020-05-12 12:48:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
// Need to revert the subscription
|
2020-05-12 17:22:04 -04:00
|
|
|
|
await subscriptionService.UpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
2020-05-12 12:48:21 -04:00
|
|
|
|
{
|
2020-05-12 17:22:04 -04:00
|
|
|
|
Items = new List<SubscriptionItemOptions>
|
2020-05-12 12:48:21 -04:00
|
|
|
|
{
|
2020-05-12 17:22:04 -04:00
|
|
|
|
new SubscriptionItemOptions
|
2020-05-12 12:48:21 -04:00
|
|
|
|
{
|
|
|
|
|
|
Id = seatItem?.Id,
|
|
|
|
|
|
Plan = plan.StripeSeatPlanId,
|
|
|
|
|
|
Quantity = organization.Seats,
|
2020-05-12 17:22:04 -04:00
|
|
|
|
Deleted = seatItem?.Id == null ? true : (bool?)null
|
2020-05-12 12:48:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// This proration behavior prevents a false "credit" from
|
|
|
|
|
|
// being applied forward to the next month's invoice
|
|
|
|
|
|
ProrationBehavior = "none",
|
2020-06-30 11:52:50 -04:00
|
|
|
|
CollectionMethod = collectionMethod,
|
2020-05-12 12:48:21 -04:00
|
|
|
|
});
|
|
|
|
|
|
throw;
|
|
|
|
|
|
}
|
2017-04-11 10:00:36 -04:00
|
|
|
|
}
|
2017-07-06 14:55:58 -04:00
|
|
|
|
|
2020-06-29 20:29:19 -04:00
|
|
|
|
// Change back the subscription collection method
|
2020-06-30 11:52:50 -04:00
|
|
|
|
if (collectionMethod != "send_invoice")
|
2020-06-29 20:29:19 -04:00
|
|
|
|
{
|
2020-06-30 11:52:50 -04:00
|
|
|
|
await subscriptionService.UpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
CollectionMethod = collectionMethod,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2020-06-29 20:29:19 -04:00
|
|
|
|
|
2017-07-06 14:55:58 -04:00
|
|
|
|
organization.Seats = (short?)newSeatTotal;
|
2020-07-07 12:01:34 -04:00
|
|
|
|
await _referenceEventService.RaiseEventAsync(
|
|
|
|
|
|
new ReferenceEvent(ReferenceEventType.AdjustSeats, organization)
|
|
|
|
|
|
{
|
|
|
|
|
|
PlanName = plan.Name,
|
|
|
|
|
|
PlanType = plan.Type,
|
|
|
|
|
|
Seats = organization.Seats,
|
|
|
|
|
|
});
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await ReplaceAndUpdateCache(organization);
|
2019-08-12 10:03:50 -04:00
|
|
|
|
return paymentIntentClientSecret;
|
2017-04-11 10:00:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-08-14 09:23:54 -04:00
|
|
|
|
public async Task VerifyBankAsync(Guid organizationId, int amount1, int amount2)
|
|
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-08-14 09:23:54 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
2017-08-14 09:23:54 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new GatewayException("Not a gateway customer.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var bankService = new BankAccountService();
|
2019-01-29 14:41:37 -05:00
|
|
|
|
var customerService = new CustomerService();
|
2017-08-14 09:23:54 -04:00
|
|
|
|
var customer = await customerService.GetAsync(organization.GatewayCustomerId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (customer == null)
|
2017-08-14 09:23:54 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new GatewayException("Cannot find customer.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var bankAccount = customer.Sources
|
2019-01-29 14:41:37 -05:00
|
|
|
|
.FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (bankAccount == null)
|
2017-08-14 09:23:54 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new GatewayException("Cannot find an unverified bank account.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = await bankService.VerifyAsync(organization.GatewayCustomerId, bankAccount.Id,
|
2019-01-29 14:41:37 -05:00
|
|
|
|
new BankAccountVerifyOptions { Amounts = new List<long> { amount1, amount2 } });
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (result.Status != "verified")
|
2017-08-14 09:23:54 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new GatewayException("Unable to verify account.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-03-27 14:36:37 -04:00
|
|
|
|
catch (StripeException e)
|
2017-08-14 09:23:54 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new GatewayException(e.Message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-03-04 21:28:41 -05:00
|
|
|
|
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup)
|
2017-03-03 00:07:11 -05:00
|
|
|
|
{
|
2017-04-07 16:41:04 -04:00
|
|
|
|
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan && !p.Disabled);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (plan == null)
|
2017-03-03 00:07:11 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Plan not found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-03-21 21:36:03 -04:00
|
|
|
|
ValidateOrganizationUpgradeParameters(plan, signup);
|
2017-04-08 10:52:10 -04:00
|
|
|
|
|
2017-03-03 00:07:11 -05:00
|
|
|
|
var organization = new Organization
|
|
|
|
|
|
{
|
2019-01-31 14:25:46 -05:00
|
|
|
|
// Pre-generate the org id so that we can save it with the Stripe subscription..
|
|
|
|
|
|
Id = CoreHelpers.GenerateComb(),
|
2017-03-04 21:28:41 -05:00
|
|
|
|
Name = signup.Name,
|
2017-04-04 12:57:50 -04:00
|
|
|
|
BillingEmail = signup.BillingEmail,
|
|
|
|
|
|
BusinessName = signup.BusinessName,
|
2017-03-03 00:07:11 -05:00
|
|
|
|
PlanType = plan.Type,
|
2017-04-10 10:44:27 -04:00
|
|
|
|
Seats = (short)(plan.BaseSeats + signup.AdditionalSeats),
|
2017-04-27 09:19:30 -04:00
|
|
|
|
MaxCollections = plan.MaxCollections,
|
2020-08-11 14:19:56 -04:00
|
|
|
|
MaxStorageGb = !plan.BaseStorageGb.HasValue ?
|
|
|
|
|
|
(short?)null : (short)(plan.BaseStorageGb.Value + signup.AdditionalStorageGb),
|
|
|
|
|
|
UsePolicies = plan.HasPolicies,
|
|
|
|
|
|
UseSso = plan.HasSso,
|
|
|
|
|
|
UseGroups = plan.HasGroups,
|
|
|
|
|
|
UseEvents = plan.HasEvents,
|
|
|
|
|
|
UseDirectory = plan.HasDirectory,
|
|
|
|
|
|
UseTotp = plan.HasTotp,
|
|
|
|
|
|
Use2fa = plan.Has2fa,
|
|
|
|
|
|
UseApi = plan.HasApi,
|
|
|
|
|
|
SelfHost = plan.HasSelfHost,
|
2018-11-20 22:02:09 -05:00
|
|
|
|
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
|
2017-04-08 16:41:40 -04:00
|
|
|
|
Plan = plan.Name,
|
2019-01-31 14:25:46 -05:00
|
|
|
|
Gateway = null,
|
2020-07-20 15:19:46 -04:00
|
|
|
|
ReferenceData = signup.Owner.ReferenceData,
|
2017-05-20 15:31:16 -04:00
|
|
|
|
Enabled = true,
|
2017-08-14 10:20:25 -04:00
|
|
|
|
LicenseKey = CoreHelpers.SecureRandomString(20),
|
2019-03-02 15:09:33 -05:00
|
|
|
|
ApiKey = CoreHelpers.SecureRandomString(30),
|
2017-03-03 00:07:11 -05:00
|
|
|
|
CreationDate = DateTime.UtcNow,
|
2020-06-25 12:28:22 -04:00
|
|
|
|
RevisionDate = DateTime.UtcNow,
|
2017-03-03 00:07:11 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (plan.Type == PlanType.Free)
|
2019-01-31 14:25:46 -05:00
|
|
|
|
{
|
|
|
|
|
|
var adminCount =
|
|
|
|
|
|
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (adminCount > 0)
|
2019-01-31 14:25:46 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You can only be an admin of one free organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2019-02-19 17:13:21 -05:00
|
|
|
|
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
2019-01-31 14:25:46 -05:00
|
|
|
|
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
2020-06-08 17:40:18 -04:00
|
|
|
|
signup.PremiumAccessAddon, signup.TaxInfo);
|
2019-01-31 14:25:46 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-07-07 12:01:34 -04:00
|
|
|
|
var returnValue = await SignUpAsync(organization, signup.Owner.Id, signup.OwnerKey, signup.CollectionName, true);
|
|
|
|
|
|
await _referenceEventService.RaiseEventAsync(
|
|
|
|
|
|
new ReferenceEvent(ReferenceEventType.Signup, organization)
|
|
|
|
|
|
{
|
|
|
|
|
|
PlanName = plan.Name,
|
|
|
|
|
|
PlanType = plan.Type,
|
|
|
|
|
|
Seats = returnValue.Item1.Seats,
|
|
|
|
|
|
Storage = returnValue.Item1.MaxStorageGb,
|
|
|
|
|
|
});
|
|
|
|
|
|
return returnValue;
|
2017-08-14 20:57:45 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(
|
2017-08-30 21:25:46 -04:00
|
|
|
|
OrganizationLicense license, User owner, string ownerKey, string collectionName)
|
2017-08-14 20:57:45 -04:00
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (license == null || !_licensingService.VerifyLicense(license))
|
2017-08-14 20:57:45 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Invalid license.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!license.CanUse(_globalSettings))
|
2017-08-16 15:43:11 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Invalid license. Make sure your license allows for on-premise " +
|
2017-08-16 15:45:38 -04:00
|
|
|
|
"hosting of organizations and that the installation id matches your current installation.");
|
2017-08-16 15:43:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (license.PlanType != PlanType.Custom &&
|
2017-10-04 16:07:34 -04:00
|
|
|
|
StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled) == null)
|
2017-08-14 20:57:45 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Plan not found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-11-06 08:12:36 -05:00
|
|
|
|
var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (enabledOrgs.Any(o => o.LicenseKey.Equals(license.LicenseKey)))
|
2017-11-06 08:12:36 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("License is already in use by another organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-08-14 20:57:45 -04:00
|
|
|
|
var organization = new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = license.Name,
|
2017-08-16 13:58:52 -04:00
|
|
|
|
BillingEmail = license.BillingEmail,
|
|
|
|
|
|
BusinessName = license.BusinessName,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
PlanType = license.PlanType,
|
|
|
|
|
|
Seats = license.Seats,
|
|
|
|
|
|
MaxCollections = license.MaxCollections,
|
2017-08-16 13:58:52 -04:00
|
|
|
|
MaxStorageGb = _globalSettings.SelfHosted ? 10240 : license.MaxStorageGb, // 10 TB
|
2020-01-15 15:00:54 -05:00
|
|
|
|
UsePolicies = license.UsePolicies,
|
2020-07-22 09:38:39 -04:00
|
|
|
|
UseSso = license.UseSso,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
UseGroups = license.UseGroups,
|
|
|
|
|
|
UseDirectory = license.UseDirectory,
|
2017-12-14 15:48:44 -05:00
|
|
|
|
UseEvents = license.UseEvents,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
UseTotp = license.UseTotp,
|
2018-04-02 14:53:19 -04:00
|
|
|
|
Use2fa = license.Use2fa,
|
2019-03-02 15:09:33 -05:00
|
|
|
|
UseApi = license.UseApi,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
Plan = license.Plan,
|
2017-08-15 16:11:08 -04:00
|
|
|
|
SelfHost = license.SelfHost,
|
2017-11-06 16:01:58 -05:00
|
|
|
|
UsersGetPremium = license.UsersGetPremium,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
Gateway = null,
|
|
|
|
|
|
GatewayCustomerId = null,
|
|
|
|
|
|
GatewaySubscriptionId = null,
|
2020-07-20 15:19:46 -04:00
|
|
|
|
ReferenceData = owner.ReferenceData,
|
2017-08-16 13:58:52 -04:00
|
|
|
|
Enabled = license.Enabled,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
ExpirationDate = license.Expires,
|
|
|
|
|
|
LicenseKey = license.LicenseKey,
|
2019-03-02 15:09:33 -05:00
|
|
|
|
ApiKey = CoreHelpers.SecureRandomString(30),
|
2017-08-14 20:57:45 -04:00
|
|
|
|
CreationDate = DateTime.UtcNow,
|
|
|
|
|
|
RevisionDate = DateTime.UtcNow
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2017-08-30 21:25:46 -04:00
|
|
|
|
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
2017-08-16 15:43:11 -04:00
|
|
|
|
|
|
|
|
|
|
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
|
|
|
|
|
Directory.CreateDirectory(dir);
|
2019-01-29 14:41:37 -05:00
|
|
|
|
System.IO.File.WriteAllText($"{dir}/{organization.Id}.json",
|
|
|
|
|
|
JsonConvert.SerializeObject(license, Formatting.Indented));
|
2017-08-16 15:43:11 -04:00
|
|
|
|
return result;
|
2017-08-14 20:57:45 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(Organization organization,
|
2017-08-30 21:25:46 -04:00
|
|
|
|
Guid ownerId, string ownerKey, string collectionName, bool withPayment)
|
2017-08-14 20:57:45 -04:00
|
|
|
|
{
|
2017-03-03 00:07:11 -05:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2017-04-04 12:57:50 -04:00
|
|
|
|
await _organizationRepository.CreateAsync(organization);
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
2017-04-04 12:57:50 -04:00
|
|
|
|
|
2017-03-03 00:07:11 -05:00
|
|
|
|
var orgUser = new OrganizationUser
|
|
|
|
|
|
{
|
|
|
|
|
|
OrganizationId = organization.Id,
|
2017-08-14 20:57:45 -04:00
|
|
|
|
UserId = ownerId,
|
|
|
|
|
|
Key = ownerKey,
|
2017-04-11 13:04:37 -04:00
|
|
|
|
Type = OrganizationUserType.Owner,
|
|
|
|
|
|
Status = OrganizationUserStatusType.Confirmed,
|
2017-04-27 15:35:26 -04:00
|
|
|
|
AccessAll = true,
|
2017-08-30 21:08:05 -04:00
|
|
|
|
CreationDate = organization.CreationDate,
|
|
|
|
|
|
RevisionDate = organization.CreationDate
|
2017-03-03 00:07:11 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await _organizationUserRepository.CreateAsync(orgUser);
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!string.IsNullOrWhiteSpace(collectionName))
|
2017-08-30 21:08:05 -04:00
|
|
|
|
{
|
2017-08-30 21:25:46 -04:00
|
|
|
|
var defaultCollection = new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = collectionName,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
CreationDate = organization.CreationDate,
|
|
|
|
|
|
RevisionDate = organization.CreationDate
|
|
|
|
|
|
};
|
|
|
|
|
|
await _collectionRepository.CreateAsync(defaultCollection);
|
|
|
|
|
|
}
|
2017-08-30 21:08:05 -04:00
|
|
|
|
|
2017-04-21 22:39:46 -04:00
|
|
|
|
// push
|
2017-08-11 08:57:31 -04:00
|
|
|
|
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
2019-03-21 21:36:03 -04:00
|
|
|
|
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds,
|
|
|
|
|
|
organization.Id.ToString());
|
2017-08-14 20:57:45 -04:00
|
|
|
|
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
|
2017-04-21 22:39:46 -04:00
|
|
|
|
|
2017-03-03 00:07:11 -05:00
|
|
|
|
return new Tuple<Organization, OrganizationUser>(organization, orgUser);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (withPayment)
|
2017-08-14 20:57:45 -04:00
|
|
|
|
{
|
2019-02-08 23:53:09 -05:00
|
|
|
|
await _paymentService.CancelAndRecoverChargesAsync(organization);
|
2017-08-14 20:57:45 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization.Id != default(Guid))
|
2017-04-04 12:57:50 -04:00
|
|
|
|
{
|
|
|
|
|
|
await _organizationRepository.DeleteAsync(organization);
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
2017-04-04 12:57:50 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-03-03 00:07:11 -05:00
|
|
|
|
throw;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2017-03-04 21:28:41 -05:00
|
|
|
|
|
2017-08-14 21:25:06 -04:00
|
|
|
|
public async Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license)
|
|
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-08-14 21:25:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!_globalSettings.SelfHosted)
|
2017-08-14 21:25:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException("Licenses require self hosting.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (license == null || !_licensingService.VerifyLicense(license))
|
2017-08-14 21:25:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Invalid license.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!license.CanUse(_globalSettings))
|
2017-08-14 21:25:06 -04:00
|
|
|
|
{
|
2017-08-16 15:43:11 -04:00
|
|
|
|
throw new BadRequestException("Invalid license. Make sure your license allows for on-premise " +
|
2017-08-16 15:45:38 -04:00
|
|
|
|
"hosting of organizations and that the installation id matches your current installation.");
|
2017-08-14 21:25:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-11-06 08:12:36 -05:00
|
|
|
|
var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (enabledOrgs.Any(o => o.LicenseKey.Equals(license.LicenseKey) && o.Id != organizationId))
|
2017-11-06 08:12:36 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("License is already in use by another organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (license.Seats.HasValue &&
|
2019-05-14 11:16:30 -04:00
|
|
|
|
(!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value))
|
2017-08-14 21:25:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (userCount > license.Seats.Value)
|
2017-08-14 21:25:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
|
|
|
|
|
|
$"Your new license only has ({ license.Seats.Value}) seats. Remove some users.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (license.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
|
2019-05-14 11:16:30 -04:00
|
|
|
|
organization.MaxCollections.Value > license.MaxCollections.Value))
|
2017-08-14 21:25:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (collectionCount > license.MaxCollections.Value)
|
2017-08-14 21:25:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
|
|
|
|
|
|
$"Your new license allows for a maximum of ({license.MaxCollections.Value}) collections. " +
|
|
|
|
|
|
"Remove some collections.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!license.UseGroups && organization.UseGroups)
|
2017-08-16 15:43:11 -04:00
|
|
|
|
{
|
|
|
|
|
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (groups.Count > 0)
|
2017-08-16 15:43:11 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Your organization currently has {groups.Count} groups. " +
|
|
|
|
|
|
$"Your new license does not allow for the use of groups. Remove all groups.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2017-08-14 21:25:06 -04:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!license.UsePolicies && organization.UsePolicies)
|
2020-01-15 15:00:54 -05:00
|
|
|
|
{
|
|
|
|
|
|
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (policies.Any(p => p.Enabled))
|
2020-01-15 15:00:54 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Your organization currently has {policies.Count} enabled " +
|
|
|
|
|
|
$"policies. Your new license does not allow for the use of policies. Disable all policies.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-08-11 14:19:56 -04:00
|
|
|
|
|
2020-07-22 09:38:39 -04:00
|
|
|
|
if (!license.UseSso && organization.UseSso)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
|
|
|
|
|
|
if (ssoConfig != null && ssoConfig.Enabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Your organization currently has a SSO configuration. " +
|
|
|
|
|
|
$"Your new license does not allow for the use of SSO. Disable your SSO configuration.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-01-15 15:00:54 -05:00
|
|
|
|
|
2017-08-14 21:25:06 -04:00
|
|
|
|
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
|
|
|
|
|
Directory.CreateDirectory(dir);
|
2019-01-29 14:41:37 -05:00
|
|
|
|
System.IO.File.WriteAllText($"{dir}/{organization.Id}.json",
|
|
|
|
|
|
JsonConvert.SerializeObject(license, Formatting.Indented));
|
2017-08-14 21:25:06 -04:00
|
|
|
|
|
|
|
|
|
|
organization.Name = license.Name;
|
2017-08-16 13:58:52 -04:00
|
|
|
|
organization.BusinessName = license.BusinessName;
|
|
|
|
|
|
organization.BillingEmail = license.BillingEmail;
|
2017-08-14 21:25:06 -04:00
|
|
|
|
organization.PlanType = license.PlanType;
|
|
|
|
|
|
organization.Seats = license.Seats;
|
|
|
|
|
|
organization.MaxCollections = license.MaxCollections;
|
|
|
|
|
|
organization.UseGroups = license.UseGroups;
|
|
|
|
|
|
organization.UseDirectory = license.UseDirectory;
|
2017-12-14 15:48:44 -05:00
|
|
|
|
organization.UseEvents = license.UseEvents;
|
2017-08-14 21:25:06 -04:00
|
|
|
|
organization.UseTotp = license.UseTotp;
|
2018-04-02 14:53:19 -04:00
|
|
|
|
organization.Use2fa = license.Use2fa;
|
2019-03-02 15:09:33 -05:00
|
|
|
|
organization.UseApi = license.UseApi;
|
2020-03-03 22:32:59 -05:00
|
|
|
|
organization.UsePolicies = license.UsePolicies;
|
2020-07-22 09:38:39 -04:00
|
|
|
|
organization.UseSso = license.UseSso;
|
2018-04-02 14:53:19 -04:00
|
|
|
|
organization.SelfHost = license.SelfHost;
|
|
|
|
|
|
organization.UsersGetPremium = license.UsersGetPremium;
|
2017-08-14 21:25:06 -04:00
|
|
|
|
organization.Plan = license.Plan;
|
2017-08-16 13:58:52 -04:00
|
|
|
|
organization.Enabled = license.Enabled;
|
2017-08-14 21:25:06 -04:00
|
|
|
|
organization.ExpirationDate = license.Expires;
|
|
|
|
|
|
organization.LicenseKey = license.LicenseKey;
|
|
|
|
|
|
organization.RevisionDate = DateTime.UtcNow;
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await ReplaceAndUpdateCache(organization);
|
2017-08-14 21:25:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-11 10:52:28 -04:00
|
|
|
|
public async Task DeleteAsync(Organization organization)
|
|
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
2017-04-11 10:52:28 -04:00
|
|
|
|
{
|
2018-09-07 14:00:56 -04:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2018-12-31 14:07:19 -05:00
|
|
|
|
var eop = !organization.ExpirationDate.HasValue ||
|
|
|
|
|
|
organization.ExpirationDate.Value >= DateTime.UtcNow;
|
2019-02-08 23:53:09 -05:00
|
|
|
|
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
2020-07-07 12:01:34 -04:00
|
|
|
|
await _referenceEventService.RaiseEventAsync(
|
|
|
|
|
|
new ReferenceEvent(ReferenceEventType.DeleteAccount, organization));
|
2018-09-07 14:00:56 -04:00
|
|
|
|
}
|
2020-03-27 14:36:37 -04:00
|
|
|
|
catch (GatewayException) { }
|
2017-04-11 10:52:28 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await _organizationRepository.DeleteAsync(organization);
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
2017-04-11 10:52:28 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-08-09 23:56:26 -04:00
|
|
|
|
public async Task EnableAsync(Guid organizationId, DateTime? expirationDate)
|
|
|
|
|
|
{
|
|
|
|
|
|
var org = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (org != null && !org.Enabled && org.Gateway.HasValue)
|
2019-08-09 23:56:26 -04:00
|
|
|
|
{
|
|
|
|
|
|
org.Enabled = true;
|
|
|
|
|
|
org.ExpirationDate = expirationDate;
|
|
|
|
|
|
org.RevisionDate = DateTime.UtcNow;
|
|
|
|
|
|
await ReplaceAndUpdateCache(org);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-08-12 22:16:42 -04:00
|
|
|
|
public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)
|
2017-04-26 16:14:15 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var org = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (org != null && org.Enabled)
|
2017-04-26 16:14:15 -04:00
|
|
|
|
{
|
|
|
|
|
|
org.Enabled = false;
|
2017-08-12 22:16:42 -04:00
|
|
|
|
org.ExpirationDate = expirationDate;
|
|
|
|
|
|
org.RevisionDate = DateTime.UtcNow;
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await ReplaceAndUpdateCache(org);
|
2017-04-26 16:14:15 -04:00
|
|
|
|
|
|
|
|
|
|
// TODO: send email to owners?
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-08-12 22:16:42 -04:00
|
|
|
|
public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate)
|
|
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var org = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (org != null)
|
2017-08-12 22:16:42 -04:00
|
|
|
|
{
|
|
|
|
|
|
org.ExpirationDate = expirationDate;
|
|
|
|
|
|
org.RevisionDate = DateTime.UtcNow;
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await ReplaceAndUpdateCache(org);
|
2017-08-12 22:16:42 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-26 16:14:15 -04:00
|
|
|
|
public async Task EnableAsync(Guid organizationId)
|
|
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var org = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (org != null && !org.Enabled)
|
2017-04-26 16:14:15 -04:00
|
|
|
|
{
|
|
|
|
|
|
org.Enabled = true;
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await ReplaceAndUpdateCache(org);
|
2017-04-26 16:14:15 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-10 19:07:38 -04:00
|
|
|
|
public async Task UpdateAsync(Organization organization, bool updateBilling = false)
|
|
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization.Id == default(Guid))
|
2017-04-10 19:07:38 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new ApplicationException("Cannot create org this way. Call SignUpAsync.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-12 16:38:22 -04:00
|
|
|
|
if (!string.IsNullOrWhiteSpace(organization.Identifier))
|
|
|
|
|
|
{
|
|
|
|
|
|
var orgById = await _organizationRepository.GetByIdentifierAsync(organization.Identifier);
|
|
|
|
|
|
if (orgById != null && orgById.Id != organization.Id)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Identifier already in use by another organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-19 16:02:39 -05:00
|
|
|
|
await ReplaceAndUpdateCache(organization, EventType.Organization_Updated);
|
2017-04-10 19:07:38 -04:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
2017-04-10 19:07:38 -04:00
|
|
|
|
{
|
2019-01-29 14:41:37 -05:00
|
|
|
|
var customerService = new CustomerService();
|
|
|
|
|
|
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
2017-04-10 19:07:38 -04:00
|
|
|
|
{
|
|
|
|
|
|
Email = organization.BillingEmail,
|
|
|
|
|
|
Description = organization.BusinessName
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-04-02 23:18:26 -04:00
|
|
|
|
public async Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)
|
|
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!type.ToString().Contains("Organization"))
|
2018-04-02 23:18:26 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentException("Not an organization provider type.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!organization.Use2fa)
|
2018-04-02 23:18:26 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Organization cannot use 2FA.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var providers = organization.GetTwoFactorProviders();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!providers?.ContainsKey(type) ?? true)
|
2018-04-02 23:18:26 -04:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
providers[type].Enabled = true;
|
|
|
|
|
|
organization.SetTwoFactorProviders(providers);
|
|
|
|
|
|
await UpdateAsync(organization);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)
|
|
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!type.ToString().Contains("Organization"))
|
2018-04-02 23:18:26 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentException("Not an organization provider type.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var providers = organization.GetTwoFactorProviders();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!providers?.ContainsKey(type) ?? true)
|
2018-04-02 23:18:26 -04:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
providers.Remove(type);
|
|
|
|
|
|
organization.SetTwoFactorProviders(providers);
|
|
|
|
|
|
await UpdateAsync(organization);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-12 13:21:15 -05:00
|
|
|
|
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
2017-05-16 00:11:21 -04:00
|
|
|
|
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
|
2017-05-18 12:04:27 -04:00
|
|
|
|
{
|
2019-05-14 11:16:30 -04:00
|
|
|
|
var results = await InviteUserAsync(organizationId, invitingUserId, new List<string> { email },
|
|
|
|
|
|
type, accessAll, externalId, collections);
|
2019-03-05 23:24:14 -05:00
|
|
|
|
var result = results.FirstOrDefault();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (result == null)
|
2019-03-05 23:24:14 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("This user has already been invited.");
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
2017-05-18 12:04:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-12 13:21:15 -05:00
|
|
|
|
public async Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
|
2017-05-18 12:04:27 -04:00
|
|
|
|
IEnumerable<string> emails, OrganizationUserType type, bool accessAll, string externalId,
|
|
|
|
|
|
IEnumerable<SelectionReadOnly> collections)
|
2017-03-04 21:28:41 -05:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-04-07 16:41:04 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (type == OrganizationUserType.Owner && invitingUserId.HasValue)
|
2017-09-27 22:37:13 -04:00
|
|
|
|
{
|
2017-12-12 13:21:15 -05:00
|
|
|
|
var invitingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(invitingUserId.Value);
|
2019-05-14 11:16:30 -04:00
|
|
|
|
var anyOwners = invitingUserOrgs.Any(
|
|
|
|
|
|
u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!anyOwners)
|
2017-09-27 22:37:13 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Only owners can invite new owners.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization.Seats.HasValue)
|
2017-04-07 16:41:04 -04:00
|
|
|
|
{
|
|
|
|
|
|
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
2017-05-18 12:04:27 -04:00
|
|
|
|
var availableSeats = organization.Seats.Value - userCount;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (availableSeats < emails.Count())
|
2017-04-07 16:41:04 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You have reached the maximum number of users " +
|
2017-04-10 10:44:27 -04:00
|
|
|
|
$"({organization.Seats.Value}) for this organization.");
|
2017-04-07 16:41:04 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-18 12:04:27 -04:00
|
|
|
|
var orgUsers = new List<OrganizationUser>();
|
2020-07-15 12:38:45 -04:00
|
|
|
|
var orgUserInvitedCount = 0;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
foreach (var email in emails)
|
2017-03-28 21:16:19 -04:00
|
|
|
|
{
|
2017-05-18 12:04:27 -04:00
|
|
|
|
// Make sure user is not already invited
|
2017-11-13 10:06:54 -05:00
|
|
|
|
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
|
|
|
|
|
|
organizationId, email, false);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (existingOrgUserCount > 0)
|
2017-05-18 12:04:27 -04:00
|
|
|
|
{
|
2018-07-09 23:07:04 -04:00
|
|
|
|
continue;
|
2017-05-18 12:04:27 -04:00
|
|
|
|
}
|
2017-03-23 00:17:34 -04:00
|
|
|
|
|
2017-05-18 12:04:27 -04:00
|
|
|
|
var orgUser = new OrganizationUser
|
|
|
|
|
|
{
|
|
|
|
|
|
OrganizationId = organizationId,
|
|
|
|
|
|
UserId = null,
|
|
|
|
|
|
Email = email.ToLowerInvariant(),
|
|
|
|
|
|
Key = null,
|
|
|
|
|
|
Type = type,
|
|
|
|
|
|
Status = OrganizationUserStatusType.Invited,
|
|
|
|
|
|
AccessAll = accessAll,
|
|
|
|
|
|
ExternalId = externalId,
|
|
|
|
|
|
CreationDate = DateTime.UtcNow,
|
|
|
|
|
|
RevisionDate = DateTime.UtcNow
|
|
|
|
|
|
};
|
2017-03-04 21:28:41 -05:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!orgUser.AccessAll && collections.Any())
|
2017-05-18 12:04:27 -04:00
|
|
|
|
{
|
|
|
|
|
|
await _organizationUserRepository.CreateAsync(orgUser, collections);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
await _organizationUserRepository.CreateAsync(orgUser);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-07-15 12:38:45 -04:00
|
|
|
|
await SendInviteAsync(orgUser, organization);
|
2018-07-09 23:07:04 -04:00
|
|
|
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Invited);
|
2017-05-18 12:04:27 -04:00
|
|
|
|
orgUsers.Add(orgUser);
|
2020-07-15 12:38:45 -04:00
|
|
|
|
orgUserInvitedCount++;
|
2017-04-20 23:50:12 -04:00
|
|
|
|
}
|
2020-07-15 12:38:45 -04:00
|
|
|
|
await _referenceEventService.RaiseEventAsync(
|
|
|
|
|
|
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization)
|
|
|
|
|
|
{
|
|
|
|
|
|
Users = orgUserInvitedCount
|
|
|
|
|
|
});
|
2017-03-04 21:28:41 -05:00
|
|
|
|
|
2017-05-18 12:04:27 -04:00
|
|
|
|
return orgUsers;
|
2017-03-04 21:28:41 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-10-07 16:23:38 -04:00
|
|
|
|
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId)
|
2017-03-23 00:17:34 -04:00
|
|
|
|
{
|
|
|
|
|
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser == null || orgUser.OrganizationId != organizationId ||
|
2017-04-12 11:00:40 -04:00
|
|
|
|
orgUser.Status != OrganizationUserStatusType.Invited)
|
2017-03-23 00:17:34 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("User invalid.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-07-15 12:38:45 -04:00
|
|
|
|
var org = await GetOrgById(orgUser.OrganizationId);
|
|
|
|
|
|
await SendInviteAsync(orgUser, org);
|
2017-03-23 00:17:34 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-07-15 12:38:45 -04:00
|
|
|
|
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization)
|
2017-03-23 00:17:34 -04:00
|
|
|
|
{
|
2017-03-23 11:51:37 -04:00
|
|
|
|
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
2017-03-23 00:17:34 -04:00
|
|
|
|
var token = _dataProtector.Protect(
|
2017-03-23 11:51:37 -04:00
|
|
|
|
$"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}");
|
2020-07-15 12:38:45 -04:00
|
|
|
|
await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, token);
|
2017-03-23 00:17:34 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-02-19 14:56:16 -05:00
|
|
|
|
public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
|
|
|
|
|
|
IUserService userService)
|
2017-03-04 21:28:41 -05:00
|
|
|
|
{
|
|
|
|
|
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser == null)
|
2017-03-04 21:28:41 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("User invalid.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-10-13 15:00:33 -05:00
|
|
|
|
if (!CoreHelpers.UserInviteTokenIsValid(_dataProtector, token, user.Email, orgUser.Id, _globalSettings))
|
2017-03-23 00:17:34 -04:00
|
|
|
|
{
|
2020-10-13 15:00:33 -05:00
|
|
|
|
throw new BadRequestException("Invalid token.");
|
2017-03-23 00:17:34 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (string.IsNullOrWhiteSpace(orgUser.Email) ||
|
2018-01-01 22:05:43 -05:00
|
|
|
|
!orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("User email does not match invite.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-10-13 15:00:33 -05:00
|
|
|
|
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
|
|
|
|
|
|
orgUser.OrganizationId, user.Email, true);
|
|
|
|
|
|
if (existingOrgUserCount > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You are already part of this organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return await AcceptUserAsync(orgUser, user, userService);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService)
|
|
|
|
|
|
{
|
|
|
|
|
|
var org = await _organizationRepository.GetByIdentifierAsync(orgIdentifier);
|
|
|
|
|
|
if (org == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Organization invalid.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
|
|
|
|
|
var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id);
|
|
|
|
|
|
if (orgUser == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("User not found within organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return await AcceptUserAsync(orgUser, user, userService);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<OrganizationUser> AcceptUserAsync(OrganizationUser orgUser, User user,
|
|
|
|
|
|
IUserService userService)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (orgUser.Status != OrganizationUserStatusType.Invited)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Already accepted.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin)
|
2017-04-07 14:03:36 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var org = await GetOrgById(orgUser.OrganizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (org.PlanType == PlanType.Free)
|
2017-09-08 17:14:15 -04:00
|
|
|
|
{
|
2019-05-14 11:16:30 -04:00
|
|
|
|
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(
|
|
|
|
|
|
user.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (adminCount > 0)
|
2017-09-08 17:14:15 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You can only be an admin of one free organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2017-04-07 14:03:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-10-20 02:48:10 -04:00
|
|
|
|
ICollection<Policy> orgPolicies = null;
|
|
|
|
|
|
ICollection<Policy> userPolicies = null;
|
|
|
|
|
|
async Task<bool> hasPolicyAsync(PolicyType policyType, bool useUserPolicies = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
var policies = useUserPolicies ?
|
|
|
|
|
|
userPolicies = userPolicies ?? await _policyRepository.GetManyByUserIdAsync(user.Id) :
|
|
|
|
|
|
orgPolicies = orgPolicies ?? await _policyRepository.GetManyByOrganizationIdAsync(orgUser.OrganizationId);
|
|
|
|
|
|
|
|
|
|
|
|
return policies.Any(p => p.Type == policyType && p.Enabled);
|
|
|
|
|
|
}
|
|
|
|
|
|
var userOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
|
|
|
|
|
if (userOrgs.Any(ou => ou.OrganizationId != orgUser.OrganizationId && ou.Status != OrganizationUserStatusType.Invited))
|
|
|
|
|
|
{
|
2020-10-27 10:28:41 -04:00
|
|
|
|
if (await hasPolicyAsync(PolicyType.SingleOrg))
|
2020-10-20 02:48:10 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You may not join this organization until you leave or remove " +
|
|
|
|
|
|
"all other organizations.");
|
|
|
|
|
|
}
|
2020-10-27 10:28:41 -04:00
|
|
|
|
if (await hasPolicyAsync(PolicyType.SingleOrg, true))
|
2020-10-20 02:48:10 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You cannot join this organization because you are a member of " +
|
|
|
|
|
|
"an organization which forbids it");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!await userService.TwoFactorIsEnabledAsync(user))
|
2020-02-19 14:56:16 -05:00
|
|
|
|
{
|
2020-10-20 02:48:10 -04:00
|
|
|
|
if (await hasPolicyAsync(PolicyType.TwoFactorAuthentication))
|
2020-02-19 14:56:16 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You cannot join this organization until you enable " +
|
|
|
|
|
|
"two-step login on your user account.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-12 11:00:40 -04:00
|
|
|
|
orgUser.Status = OrganizationUserStatusType.Accepted;
|
2017-03-23 16:56:25 -04:00
|
|
|
|
orgUser.UserId = user.Id;
|
2017-03-04 21:28:41 -05:00
|
|
|
|
orgUser.Email = null;
|
2020-10-13 15:00:33 -05:00
|
|
|
|
|
2017-03-04 21:28:41 -05:00
|
|
|
|
await _organizationUserRepository.ReplaceAsync(orgUser);
|
|
|
|
|
|
|
2017-04-21 14:22:32 -04:00
|
|
|
|
// TODO: send notification emails to org admins and accepting user?
|
2017-03-04 21:28:41 -05:00
|
|
|
|
return orgUser;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-03-23 00:17:34 -04:00
|
|
|
|
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
2020-03-09 15:13:40 -04:00
|
|
|
|
Guid confirmingUserId, IUserService userService)
|
2017-03-04 21:28:41 -05:00
|
|
|
|
{
|
|
|
|
|
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Accepted ||
|
2017-03-23 00:17:34 -04:00
|
|
|
|
orgUser.OrganizationId != organizationId)
|
2017-03-04 21:28:41 -05:00
|
|
|
|
{
|
2017-03-23 00:17:34 -04:00
|
|
|
|
throw new BadRequestException("User not valid.");
|
2017-03-04 21:28:41 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var org = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (org.PlanType == PlanType.Free &&
|
2017-09-08 17:14:15 -04:00
|
|
|
|
(orgUser.Type == OrganizationUserType.Admin || orgUser.Type == OrganizationUserType.Owner))
|
|
|
|
|
|
{
|
|
|
|
|
|
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(
|
|
|
|
|
|
orgUser.UserId.Value);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (adminCount > 0)
|
2017-09-08 17:14:15 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("User can only be an admin of one free organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-09 15:13:40 -04:00
|
|
|
|
var user = await _userRepository.GetByIdAsync(orgUser.UserId.Value);
|
|
|
|
|
|
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
|
|
|
|
|
|
var usingTwoFactorPolicy = policies.Any(p => p.Type == PolicyType.TwoFactorAuthentication && p.Enabled);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (usingTwoFactorPolicy && !(await userService.TwoFactorIsEnabledAsync(user)))
|
2020-03-09 15:13:40 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("User does not have two-step login enabled.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-10-27 10:28:41 -04:00
|
|
|
|
var usingSingleOrgPolicy = policies.Any(p => p.Type == PolicyType.SingleOrg && p.Enabled);
|
|
|
|
|
|
if (usingSingleOrgPolicy)
|
2020-10-20 02:48:10 -04:00
|
|
|
|
{
|
|
|
|
|
|
var userOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
|
|
|
|
|
if (userOrgs.Any(ou => ou.OrganizationId != organizationId && ou.Status != OrganizationUserStatusType.Invited))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("User is a member of another organization.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-12 11:00:40 -04:00
|
|
|
|
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
2017-03-04 21:28:41 -05:00
|
|
|
|
orgUser.Key = key;
|
|
|
|
|
|
orgUser.Email = null;
|
|
|
|
|
|
await _organizationUserRepository.ReplaceAsync(orgUser);
|
2017-12-01 16:00:30 -05:00
|
|
|
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
2017-09-08 17:14:15 -04:00
|
|
|
|
await _mailService.SendOrganizationConfirmedEmailAsync(org.Name, user.Email);
|
2017-03-04 21:28:41 -05:00
|
|
|
|
|
2017-04-21 14:22:32 -04:00
|
|
|
|
// push
|
2017-08-11 08:57:31 -04:00
|
|
|
|
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
|
|
|
|
|
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds, organizationId.ToString());
|
2017-05-26 22:52:50 -04:00
|
|
|
|
await _pushNotificationService.PushSyncOrgKeysAsync(orgUser.UserId.Value);
|
2017-04-21 14:22:32 -04:00
|
|
|
|
|
2017-03-04 21:28:41 -05:00
|
|
|
|
return orgUser;
|
|
|
|
|
|
}
|
2017-03-09 23:58:43 -05:00
|
|
|
|
|
2019-03-05 23:24:14 -05:00
|
|
|
|
public async Task SaveUserAsync(OrganizationUser user, Guid? savingUserId,
|
|
|
|
|
|
IEnumerable<SelectionReadOnly> collections)
|
2017-03-09 23:58:43 -05:00
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (user.Id.Equals(default(Guid)))
|
2017-03-09 23:58:43 -05:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Invite the user first.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (savingUserId.HasValue)
|
2017-09-27 22:37:13 -04:00
|
|
|
|
{
|
2019-03-05 23:24:14 -05:00
|
|
|
|
var savingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(savingUserId.Value);
|
2019-03-28 12:36:57 -04:00
|
|
|
|
var savingUserIsOrgOwner = savingUserOrgs
|
|
|
|
|
|
.Any(u => u.OrganizationId == user.OrganizationId && u.Type == OrganizationUserType.Owner);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!savingUserIsOrgOwner)
|
2017-09-27 22:37:13 -04:00
|
|
|
|
{
|
2019-03-28 12:36:57 -04:00
|
|
|
|
var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id);
|
|
|
|
|
|
var isOwner = originalUser.Type == OrganizationUserType.Owner;
|
|
|
|
|
|
var nowOwner = user.Type == OrganizationUserType.Owner;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if ((isOwner && !nowOwner) || (!isOwner && nowOwner))
|
2019-03-28 12:36:57 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Only an owner can change the user type of another owner.");
|
|
|
|
|
|
}
|
2017-09-27 22:37:13 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-03-29 21:26:19 -04:00
|
|
|
|
var confirmedOwners = (await GetConfirmedOwnersAsync(user.OrganizationId)).ToList();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (user.Type != OrganizationUserType.Owner &&
|
2019-05-14 11:16:30 -04:00
|
|
|
|
confirmedOwners.Count == 1 && confirmedOwners[0].Id == user.Id)
|
2017-03-29 21:26:19 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (user.AccessAll)
|
2017-04-20 23:50:12 -04:00
|
|
|
|
{
|
2017-04-27 09:19:30 -04:00
|
|
|
|
// We don't need any collections if we're flagged to have all access.
|
2017-05-11 14:52:35 -04:00
|
|
|
|
collections = new List<SelectionReadOnly>();
|
2017-04-20 23:50:12 -04:00
|
|
|
|
}
|
2017-05-11 14:52:35 -04:00
|
|
|
|
await _organizationUserRepository.ReplaceAsync(user, collections);
|
2017-12-01 16:00:30 -05:00
|
|
|
|
await _eventService.LogOrganizationUserEventAsync(user, EventType.OrganizationUser_Updated);
|
2017-03-11 22:42:27 -05:00
|
|
|
|
}
|
2017-03-09 23:58:43 -05:00
|
|
|
|
|
2017-12-12 13:21:15 -05:00
|
|
|
|
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
2017-03-23 00:17:34 -04:00
|
|
|
|
{
|
|
|
|
|
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
2017-03-23 00:17:34 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("User not valid.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)
|
2017-04-18 15:27:54 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You cannot remove yourself.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue)
|
2017-09-27 22:37:13 -04:00
|
|
|
|
{
|
2017-12-12 13:21:15 -05:00
|
|
|
|
var deletingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(deletingUserId.Value);
|
2019-05-14 11:16:30 -04:00
|
|
|
|
var anyOwners = deletingUserOrgs.Any(
|
|
|
|
|
|
u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!anyOwners)
|
2017-09-27 22:37:13 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Only owners can delete other owners.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-03-29 21:26:19 -04:00
|
|
|
|
var confirmedOwners = (await GetConfirmedOwnersAsync(organizationId)).ToList();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (confirmedOwners.Count == 1 && confirmedOwners[0].Id == organizationUserId)
|
2017-03-29 21:26:19 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-04-12 10:07:27 -04:00
|
|
|
|
await _organizationUserRepository.DeleteAsync(orgUser);
|
2017-12-01 16:00:30 -05:00
|
|
|
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
|
2017-05-26 22:52:50 -04:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser.UserId.HasValue)
|
2017-07-10 16:38:18 -04:00
|
|
|
|
{
|
|
|
|
|
|
// push
|
2017-08-11 08:57:31 -04:00
|
|
|
|
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
2019-05-14 11:16:30 -04:00
|
|
|
|
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds,
|
|
|
|
|
|
organizationId.ToString());
|
2018-08-28 08:52:20 -04:00
|
|
|
|
await _pushNotificationService.PushSyncOrgKeysAsync(orgUser.UserId.Value);
|
2017-07-10 16:38:18 -04:00
|
|
|
|
}
|
2017-04-12 10:07:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task DeleteUserAsync(Guid organizationId, Guid userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser == null)
|
2017-04-12 10:07:27 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var confirmedOwners = (await GetConfirmedOwnersAsync(organizationId)).ToList();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (confirmedOwners.Count == 1 && confirmedOwners[0].Id == orgUser.Id)
|
2017-04-12 10:07:27 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-03-23 00:17:34 -04:00
|
|
|
|
await _organizationUserRepository.DeleteAsync(orgUser);
|
2017-12-01 16:00:30 -05:00
|
|
|
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
|
2017-05-26 22:52:50 -04:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser.UserId.HasValue)
|
2017-07-10 16:38:18 -04:00
|
|
|
|
{
|
|
|
|
|
|
// push
|
2017-08-11 08:57:31 -04:00
|
|
|
|
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
2019-05-14 11:16:30 -04:00
|
|
|
|
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds,
|
|
|
|
|
|
organizationId.ToString());
|
2018-08-28 08:52:20 -04:00
|
|
|
|
await _pushNotificationService.PushSyncOrgKeysAsync(orgUser.UserId.Value);
|
2017-07-10 16:38:18 -04:00
|
|
|
|
}
|
2017-03-23 00:17:34 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-01 16:00:30 -05:00
|
|
|
|
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
|
2019-05-14 11:16:30 -04:00
|
|
|
|
await _eventService.LogOrganizationUserEventAsync(organizationUser,
|
|
|
|
|
|
EventType.OrganizationUser_UpdatedGroups);
|
2017-12-01 16:00:30 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-08-15 16:11:08 -04:00
|
|
|
|
public async Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId)
|
|
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2017-08-30 11:23:55 -04:00
|
|
|
|
return await GenerateLicenseAsync(organization, installationId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-18 17:00:21 -04:00
|
|
|
|
public async Task<OrganizationLicense> GenerateLicenseAsync(Organization organization, Guid installationId,
|
|
|
|
|
|
int? version = null)
|
2017-08-30 11:23:55 -04:00
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-08-15 16:11:08 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var installation = await _installationRepository.GetByIdAsync(installationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (installation == null || !installation.Enabled)
|
2017-08-15 16:11:08 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Invalid installation id");
|
|
|
|
|
|
}
|
2019-02-19 17:13:21 -05:00
|
|
|
|
|
2019-02-18 15:40:47 -05:00
|
|
|
|
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
|
2020-08-20 10:12:27 -04:00
|
|
|
|
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
|
2017-08-15 16:11:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
public async Task ImportAsync(Guid organizationId,
|
2020-04-23 11:29:19 -04:00
|
|
|
|
Guid? importingUserId,
|
2017-05-16 00:11:21 -04:00
|
|
|
|
IEnumerable<ImportedGroup> groups,
|
|
|
|
|
|
IEnumerable<ImportedOrganizationUser> newUsers,
|
2019-05-06 21:31:20 -04:00
|
|
|
|
IEnumerable<string> removeUserExternalIds,
|
|
|
|
|
|
bool overwriteExisting)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2017-12-19 16:02:39 -05:00
|
|
|
|
var organization = await GetOrgById(organizationId);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization == null)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new NotFoundException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!organization.UseDirectory)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2017-05-20 15:31:16 -04:00
|
|
|
|
throw new BadRequestException("Organization cannot use directory syncing.");
|
2017-05-13 12:00:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-20 15:31:16 -04:00
|
|
|
|
var newUsersSet = new HashSet<string>(newUsers?.Select(u => u.ExternalId) ?? new List<string>());
|
2017-05-18 10:41:47 -04:00
|
|
|
|
var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
|
|
|
|
|
var existingExternalUsers = existingUsers.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
|
|
|
|
|
|
var existingExternalUsersIdDict = existingExternalUsers.ToDictionary(u => u.ExternalId, u => u.Id);
|
2017-05-13 12:00:40 -04:00
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
// Users
|
2017-11-13 12:09:39 -05:00
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
// Remove Users
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (removeUserExternalIds?.Any() ?? false)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2017-05-16 00:11:21 -04:00
|
|
|
|
var removeUsersSet = new HashSet<string>(removeUserExternalIds);
|
2017-05-18 10:41:47 -04:00
|
|
|
|
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
|
2017-05-13 14:14:20 -04:00
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
var usersToRemove = removeUsersSet
|
|
|
|
|
|
.Except(newUsersSet)
|
|
|
|
|
|
.Where(ru => existingUsersDict.ContainsKey(ru))
|
|
|
|
|
|
.Select(ru => existingUsersDict[ru]);
|
2017-05-13 14:14:20 -04:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
foreach (var user in usersToRemove)
|
2017-05-13 14:14:20 -04:00
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (user.Type != OrganizationUserType.Owner)
|
2019-05-14 13:09:56 -04:00
|
|
|
|
{
|
|
|
|
|
|
await _organizationUserRepository.DeleteAsync(new OrganizationUser { Id = user.Id });
|
|
|
|
|
|
existingExternalUsersIdDict.Remove(user.ExternalId);
|
|
|
|
|
|
}
|
2017-05-13 14:14:20 -04:00
|
|
|
|
}
|
2017-05-13 12:00:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (overwriteExisting)
|
2019-05-06 21:31:20 -04:00
|
|
|
|
{
|
|
|
|
|
|
// Remove existing external users that are not in new user set
|
2020-03-27 14:36:37 -04:00
|
|
|
|
foreach (var user in existingExternalUsers)
|
2019-05-06 21:31:20 -04:00
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (user.Type != OrganizationUserType.Owner && !newUsersSet.Contains(user.ExternalId) &&
|
2019-05-06 21:31:20 -04:00
|
|
|
|
existingExternalUsersIdDict.ContainsKey(user.ExternalId))
|
|
|
|
|
|
{
|
2019-05-14 13:09:56 -04:00
|
|
|
|
await _organizationUserRepository.DeleteAsync(new OrganizationUser { Id = user.Id });
|
2019-05-06 21:31:20 -04:00
|
|
|
|
existingExternalUsersIdDict.Remove(user.ExternalId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (newUsers?.Any() ?? false)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2017-11-10 15:22:19 -05:00
|
|
|
|
// Marry existing users
|
|
|
|
|
|
var existingUsersEmailsDict = existingUsers
|
|
|
|
|
|
.Where(u => string.IsNullOrWhiteSpace(u.ExternalId))
|
|
|
|
|
|
.ToDictionary(u => u.Email);
|
|
|
|
|
|
var newUsersEmailsDict = newUsers.ToDictionary(u => u.Email);
|
|
|
|
|
|
var usersToAttach = existingUsersEmailsDict.Keys.Intersect(newUsersEmailsDict.Keys).ToList();
|
2020-03-27 14:36:37 -04:00
|
|
|
|
foreach (var user in usersToAttach)
|
2017-11-10 15:22:19 -05:00
|
|
|
|
{
|
|
|
|
|
|
var orgUserDetails = existingUsersEmailsDict[user];
|
|
|
|
|
|
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserDetails.Id);
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgUser != null)
|
2017-11-10 15:22:19 -05:00
|
|
|
|
{
|
|
|
|
|
|
orgUser.ExternalId = newUsersEmailsDict[user].ExternalId;
|
|
|
|
|
|
await _organizationUserRepository.UpsertAsync(orgUser);
|
|
|
|
|
|
existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add new users
|
|
|
|
|
|
var existingUsersSet = new HashSet<string>(existingExternalUsersIdDict.Keys);
|
2017-05-15 14:41:20 -04:00
|
|
|
|
var usersToAdd = newUsersSet.Except(existingUsersSet).ToList();
|
2017-05-13 12:00:40 -04:00
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
var seatsAvailable = int.MaxValue;
|
2017-05-18 08:58:08 -04:00
|
|
|
|
var enoughSeatsAvailable = true;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (organization.Seats.HasValue)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2017-05-15 14:41:20 -04:00
|
|
|
|
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
|
|
|
|
|
seatsAvailable = organization.Seats.Value - userCount;
|
2017-05-18 08:58:08 -04:00
|
|
|
|
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
2017-05-13 12:00:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (enoughSeatsAvailable)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
foreach (var user in newUsers)
|
2017-05-16 00:11:21 -04:00
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email))
|
2017-05-18 08:58:08 -04:00
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2017-05-16 00:11:21 -04:00
|
|
|
|
|
2017-05-18 08:58:08 -04:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var newUser = await InviteUserAsync(organizationId, importingUserId, user.Email,
|
|
|
|
|
|
OrganizationUserType.User, false, user.ExternalId, new List<SelectionReadOnly>());
|
2017-05-18 10:41:47 -04:00
|
|
|
|
existingExternalUsersIdDict.Add(newUser.ExternalId, newUser.Id);
|
2017-05-18 08:58:08 -04:00
|
|
|
|
}
|
2020-03-27 14:36:37 -04:00
|
|
|
|
catch (BadRequestException)
|
2017-05-18 08:58:08 -04:00
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2017-05-13 12:00:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
// Groups
|
2017-11-13 12:09:39 -05:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (groups?.Any() ?? false)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (!organization.UseGroups)
|
2017-05-20 15:31:16 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Organization cannot use groups.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-16 00:11:21 -04:00
|
|
|
|
var groupsDict = groups.ToDictionary(g => g.Group.ExternalId);
|
2017-05-18 10:41:47 -04:00
|
|
|
|
var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
2019-05-14 11:16:30 -04:00
|
|
|
|
var existingExternalGroups = existingGroups
|
|
|
|
|
|
.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
|
2017-05-18 10:41:47 -04:00
|
|
|
|
var existingExternalGroupsDict = existingExternalGroups.ToDictionary(g => g.ExternalId);
|
2017-05-15 14:41:20 -04:00
|
|
|
|
|
|
|
|
|
|
var newGroups = groups
|
2017-05-18 10:41:47 -04:00
|
|
|
|
.Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId))
|
2017-05-16 00:11:21 -04:00
|
|
|
|
.Select(g => g.Group);
|
2017-05-15 14:41:20 -04:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
foreach (var group in newGroups)
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2017-05-15 14:41:20 -04:00
|
|
|
|
group.CreationDate = group.RevisionDate = DateTime.UtcNow;
|
2017-05-13 12:00:40 -04:00
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
await _groupRepository.CreateAsync(group);
|
2019-05-14 11:16:30 -04:00
|
|
|
|
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
|
|
|
|
|
|
existingExternalUsersIdDict);
|
2017-05-15 14:41:20 -04:00
|
|
|
|
}
|
2017-05-13 14:14:20 -04:00
|
|
|
|
|
2017-05-18 10:41:47 -04:00
|
|
|
|
var updateGroups = existingExternalGroups
|
2017-05-15 16:37:56 -04:00
|
|
|
|
.Where(g => groupsDict.ContainsKey(g.ExternalId))
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (updateGroups.Any())
|
2017-05-13 12:00:40 -04:00
|
|
|
|
{
|
2017-05-15 16:37:56 -04:00
|
|
|
|
var groupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organizationId);
|
|
|
|
|
|
var existingGroupUsers = groupUsers
|
2017-05-15 15:01:16 -04:00
|
|
|
|
.GroupBy(gu => gu.GroupId)
|
2017-05-15 16:37:56 -04:00
|
|
|
|
.ToDictionary(g => g.Key, g => new HashSet<Guid>(g.Select(gr => gr.OrganizationUserId)));
|
2017-05-15 14:41:20 -04:00
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
foreach (var group in updateGroups)
|
2017-05-15 15:01:16 -04:00
|
|
|
|
{
|
2017-05-16 00:11:21 -04:00
|
|
|
|
var updatedGroup = groupsDict[group.ExternalId].Group;
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (group.Name != updatedGroup.Name)
|
2017-05-15 16:37:56 -04:00
|
|
|
|
{
|
|
|
|
|
|
group.RevisionDate = DateTime.UtcNow;
|
|
|
|
|
|
group.Name = updatedGroup.Name;
|
|
|
|
|
|
|
|
|
|
|
|
await _groupRepository.ReplaceAsync(group);
|
|
|
|
|
|
}
|
2017-05-15 15:01:16 -04:00
|
|
|
|
|
2019-05-14 11:16:30 -04:00
|
|
|
|
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
|
|
|
|
|
|
existingExternalUsersIdDict,
|
2017-05-15 16:37:56 -04:00
|
|
|
|
existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null);
|
2017-05-15 15:01:16 -04:00
|
|
|
|
}
|
2017-05-13 12:00:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-03-04 09:52:43 -05:00
|
|
|
|
public async Task RotateApiKeyAsync(Organization organization)
|
|
|
|
|
|
{
|
|
|
|
|
|
organization.ApiKey = CoreHelpers.SecureRandomString(30);
|
|
|
|
|
|
organization.RevisionDate = DateTime.UtcNow;
|
|
|
|
|
|
await ReplaceAndUpdateCache(organization);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-26 14:12:04 -04:00
|
|
|
|
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _ssoUserRepository.DeleteAsync(userId, organizationId);
|
|
|
|
|
|
if (organizationId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId);
|
|
|
|
|
|
if (organizationUser != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UnlinkedSso);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,
|
2017-05-15 15:01:16 -04:00
|
|
|
|
Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid> existingUsers = null)
|
2017-05-15 14:41:20 -04:00
|
|
|
|
{
|
2017-05-15 16:37:56 -04:00
|
|
|
|
var availableUsers = groupUsers.Intersect(existingUsersIdDict.Keys);
|
|
|
|
|
|
var users = new HashSet<Guid>(availableUsers.Select(u => existingUsersIdDict[u]));
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (existingUsers != null && existingUsers.Count == users.Count && users.SetEquals(existingUsers))
|
2017-05-15 15:01:16 -04:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-15 14:41:20 -04:00
|
|
|
|
await _groupRepository.UpdateUsersAsync(group.Id, users);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-03-29 21:26:19 -04:00
|
|
|
|
private async Task<IEnumerable<OrganizationUser>> GetConfirmedOwnersAsync(Guid organizationId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId,
|
2017-05-13 12:00:40 -04:00
|
|
|
|
OrganizationUserType.Owner);
|
|
|
|
|
|
return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed);
|
2017-03-29 21:26:19 -04:00
|
|
|
|
}
|
2017-08-11 08:57:31 -04:00
|
|
|
|
|
|
|
|
|
|
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
2017-11-14 08:39:16 -05:00
|
|
|
|
return devices.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)).Select(d => d.Id.ToString());
|
2017-08-11 08:57:31 -04:00
|
|
|
|
}
|
2017-12-19 16:02:39 -05:00
|
|
|
|
|
|
|
|
|
|
private async Task ReplaceAndUpdateCache(Organization org, EventType? orgEvent = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _organizationRepository.ReplaceAsync(org);
|
|
|
|
|
|
await _applicationCacheService.UpsertOrganizationAbilityAsync(org);
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (orgEvent.HasValue)
|
2017-12-19 16:02:39 -05:00
|
|
|
|
{
|
|
|
|
|
|
await _eventService.LogOrganizationEventAsync(org, orgEvent.Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<Organization> GetOrgById(Guid id)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await _organizationRepository.GetByIdAsync(id);
|
|
|
|
|
|
}
|
2019-03-21 21:36:03 -04:00
|
|
|
|
|
|
|
|
|
|
private void ValidateOrganizationUpgradeParameters(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
|
|
|
|
|
{
|
2020-08-11 14:19:56 -04:00
|
|
|
|
if (!plan.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
|
2019-03-21 21:36:03 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Plan does not allow additional storage.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (upgrade.AdditionalStorageGb < 0)
|
2019-03-21 21:36:03 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You can't subtract storage!");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-11 14:19:56 -04:00
|
|
|
|
if (!plan.HasPremiumAccessOption && upgrade.PremiumAccessAddon)
|
2019-03-21 21:36:03 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (plan.BaseSeats + upgrade.AdditionalSeats <= 0)
|
2019-03-21 21:36:03 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You do not have any seats!");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-27 14:36:37 -04:00
|
|
|
|
if (upgrade.AdditionalSeats < 0)
|
2019-03-21 21:36:03 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("You can't subtract seats!");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-11 14:19:56 -04:00
|
|
|
|
if (!plan.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
|
2019-03-21 21:36:03 -04:00
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException("Plan does not allow additional users.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-11 14:19:56 -04:00
|
|
|
|
if (plan.HasAdditionalSeatsOption && plan.MaxAdditionalSeats.HasValue &&
|
2019-03-21 21:36:03 -04:00
|
|
|
|
upgrade.AdditionalSeats > plan.MaxAdditionalSeats.Value)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BadRequestException($"Selected plan allows a maximum of " +
|
|
|
|
|
|
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2017-03-03 00:07:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|