1
0
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:
Cy Okeke 2024-11-12 11:23:28 +01:00
parent ef4bedb616
commit 00d041837f
No known key found for this signature in database
GPG Key ID: 88B341B55C84B45C
13 changed files with 141 additions and 3 deletions
src
Api/Billing/Controllers
Core
Infrastructure.Dapper/Repositories
Infrastructure.EntityFramework/Repositories

View File

@ -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);
}

View File

@ -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"
};
}
}

View File

@ -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);

View File

@ -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()
{

View File

@ -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>Heres 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}}

View File

@ -0,0 +1,9 @@
{{#>BasicTextLayout}}
{{SponsoringOrgName}} has removed the Free Bitwarden Families plan sponsorship.
Heres 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}}

View File

@ -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";
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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;
}
}