mirror of
https://github.com/bitwarden/server.git
synced 2025-04-02 18:06:07 +02:00
Changes for email notification
This commit is contained in:
parent
ef4bedb616
commit
00d041837f
src
Api/Billing/Controllers
Core
AdminConsole
Constants.csMailTemplates/Handlebars/FamiliesForEnterprise
FamiliesForEnterpriseRemovedFromFamilyUser.html.hbsFamiliesForEnterpriseRemovedFromFamilyUser.text.hbs
Models/Mail/FamiliesForEnterprise
Repositories
Services
Infrastructure.Dapper/Repositories
Infrastructure.EntityFramework/Repositories
@ -1,6 +1,9 @@
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -31,6 +34,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
|
||||
public OrganizationSponsorshipsController(
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
@ -45,7 +49,8 @@ public class OrganizationSponsorshipsController : Controller
|
||||
IRemoveSponsorshipCommand removeSponsorshipCommand,
|
||||
ICloudSyncSponsorshipsCommand syncSponsorshipsCommand,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext)
|
||||
ICurrentContext currentContext,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -60,6 +65,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
_syncSponsorshipsCommand = syncSponsorshipsCommand;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_policyRepository = policyRepository;
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
@ -187,5 +193,21 @@ public class OrganizationSponsorshipsController : Controller
|
||||
return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate);
|
||||
}
|
||||
|
||||
[HttpGet("{sponsoredEmail}")]
|
||||
public async Task<IResult> GetSponsoringSponsoredEmailAsync(string sponsoredEmail)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sponsoredEmail))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var sponsorship = await _organizationSponsorshipRepository.GetBySponsoredOrganizationUserEmailAsync(sponsoredEmail);
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync((Guid)sponsorship.SponsoringOrganizationId,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
var response = OrganizationSponsorshipResponse.From(policy);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
private Task<User> CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ public enum PolicyType : byte
|
||||
DisablePersonalVaultExport = 10,
|
||||
ActivateAutofill = 11,
|
||||
AutomaticAppLogIn = 12,
|
||||
FreeFamiliesSponsorshipPolicy = 13
|
||||
}
|
||||
|
||||
public static class PolicyTypeExtensions
|
||||
@ -40,6 +41,7 @@ public static class PolicyTypeExtensions
|
||||
PolicyType.DisablePersonalVaultExport => "Remove individual vault export",
|
||||
PolicyType.ActivateAutofill => "Active auto-fill",
|
||||
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ public class PolicyService : IPolicyService
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
|
||||
|
||||
public PolicyService(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
@ -48,7 +49,8 @@ public class PolicyService : IPolicyService
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
@ -63,6 +65,7 @@ public class PolicyService : IPolicyService
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Policy policy, Guid? savingUserId)
|
||||
@ -116,6 +119,10 @@ public class PolicyService : IPolicyService
|
||||
}
|
||||
|
||||
await EnablePolicyAsync(policy, org, savingUserId);
|
||||
if (policy.Type == PolicyType.FreeFamiliesSponsorshipPolicy && _featureService.IsEnabled(FeatureFlagKeys.DisableFreeFamiliesSponsorship))
|
||||
{
|
||||
await NotifiesUserWithApplicablePoliciesAsync(policy, org.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||
@ -151,6 +158,25 @@ public class PolicyService : IPolicyService
|
||||
return result.Any();
|
||||
}
|
||||
|
||||
private async Task NotifiesUserWithApplicablePoliciesAsync(Policy policy, string organizationName)
|
||||
{
|
||||
var organizationSponsorships = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(policy.OrganizationId))
|
||||
.Where(p => p.SponsoredOrganizationId is not null)
|
||||
.ToList();
|
||||
if (string.IsNullOrWhiteSpace(organizationName))
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
|
||||
organizationName = organization?.Name;
|
||||
}
|
||||
|
||||
foreach (var org in organizationSponsorships)
|
||||
{
|
||||
var offerAcceptanceDate = org.ValidUntil!.Value.AddDays(-7).ToString("MM/dd/yyyy");
|
||||
await _mailService.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(org.FriendlyName, offerAcceptanceDate,
|
||||
org.SponsoredOrganizationId.ToString(), organizationName);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<OrganizationUserPolicyDetails>> QueryOrganizationUserPolicyDetailsAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)
|
||||
{
|
||||
var organizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId, policyType);
|
||||
|
@ -148,6 +148,7 @@ public static class FeatureFlagKeys
|
||||
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -0,0 +1,22 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
{{SponsoringOrgName}} has removed the Free Bitwarden Families plan sponsorship.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
<strong>Here’s what that means:</strong></br>
|
||||
Your Free Bitwarden Families sponsorship will charge your stored payment method on {{OfferAcceptanceDate}}. To avoid any disruption in your service, please ensure your payment method on the <a target="_blank" clicktracking=off href="{{SubscriptionUrl}}" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">Subscription page</a> is up to date.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
Contact your organization administrators for more information.
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,9 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{SponsoringOrgName}} has removed the Free Bitwarden Families plan sponsorship.
|
||||
|
||||
Here’s what that means:
|
||||
Your Free Bitwarden Families sponsorship will charge your stored payment method on {{OfferAcceptanceDate}}. To avoid any disruption in your service, please ensure your payment method on the Subscription page is up to date. Or click the following link: {{{link SubscriptionUrl}}}
|
||||
|
||||
Contact your organization administrators for more information.
|
||||
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Models.Mail.FamiliesForEnterprise;
|
||||
|
||||
public class FamiliesForEnterpriseRemoveOfferViewModel : BaseMailModel
|
||||
{
|
||||
public string SponsoringOrgName { get; set; }
|
||||
public string SponsoredOrganizationId { get; set; }
|
||||
public string OfferAcceptanceDate { get; set; }
|
||||
public string SubscriptionUrl =>
|
||||
$"{WebVaultUrl}/organizations/{SponsoredOrganizationId}/billing/subscription";
|
||||
}
|
@ -14,4 +14,5 @@ public interface IOrganizationSponsorshipRepository : IRepository<OrganizationSp
|
||||
Task<OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId);
|
||||
Task<OrganizationSponsorship?> GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId);
|
||||
Task<DateTime?> GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId);
|
||||
Task<OrganizationSponsorship?> GetBySponsoredOrganizationUserEmailAsync(string email);
|
||||
}
|
||||
|
@ -76,6 +76,8 @@ public interface IMailService
|
||||
Task SendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, string email, bool existingAccount, string token);
|
||||
Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites);
|
||||
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
|
||||
Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,
|
||||
string organizationName);
|
||||
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
|
||||
Task SendOTPEmailAsync(string email, string token);
|
||||
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
||||
|
@ -937,6 +937,22 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,
|
||||
string organizationName)
|
||||
{
|
||||
var message = CreateDefaultMessage("Removal of Free Bitwarden Families plan", email);
|
||||
var model = new FamiliesForEnterpriseRemoveOfferViewModel
|
||||
{
|
||||
SponsoredOrganizationId = organizationId,
|
||||
SponsoringOrgName = CoreHelpers.SanitizeForEmail(organizationName),
|
||||
OfferAcceptanceDate = offerAcceptanceDate,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash
|
||||
};
|
||||
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRemovedFromFamilyUser", model);
|
||||
message.Category = "FamiliesForEnterpriseRemovedFromFamilyUser";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate)
|
||||
{
|
||||
var message = CreateDefaultMessage("Your Families Sponsorship was Removed", email);
|
||||
|
@ -236,6 +236,13 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate,
|
||||
string organizationId,
|
||||
string organizationName)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
@ -145,4 +145,15 @@ public class OrganizationSponsorshipRepository : Repository<OrganizationSponsors
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrganizationSponsorship?> GetBySponsoredOrganizationUserEmailAsync(string email)
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
var results = await connection.QueryAsync<OrganizationSponsorship>(
|
||||
"[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]",
|
||||
new { OfferedToEmail = email },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.SingleOrDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -140,4 +140,13 @@ public class OrganizationSponsorshipRepository : Repository<Core.Entities.Organi
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Core.Entities.OrganizationSponsorship?> GetBySponsoredOrganizationUserEmailAsync(string email)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var orgSponsorship = await GetDbSet(dbContext).Where(e => e.OfferedToEmail == email)
|
||||
.FirstOrDefaultAsync();
|
||||
return orgSponsorship;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user