mirror of
https://github.com/bitwarden/server
synced 2025-12-26 05:03:18 +00:00
Merge branch 'SM-1571-DisableSMAdsForUsers' of https://github.com/bitwarden/server into SM-1571-DisableSMAdsForUsers
This commit is contained in:
@@ -3,6 +3,7 @@ using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -50,7 +51,8 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
ISubscriberService subscriberService,
|
||||
IUserService userService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger)
|
||||
ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger,
|
||||
IPricingClient pricingClient)
|
||||
: BaseBillingCommand<CreatePremiumCloudHostedSubscriptionCommand>(logger), ICreatePremiumCloudHostedSubscriptionCommand
|
||||
{
|
||||
private static readonly List<string> _expand = ["tax"];
|
||||
@@ -255,11 +257,13 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
Customer customer,
|
||||
int? storage)
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Price = StripeConstants.Prices.PremiumAnnually,
|
||||
Price = premiumPlan.Seat.StripePriceId,
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
@@ -268,7 +272,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = StripeConstants.Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = storage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Premium.Commands;
|
||||
|
||||
using static StripeConstants;
|
||||
|
||||
public interface IPreviewPremiumTaxCommand
|
||||
{
|
||||
Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(
|
||||
@@ -18,6 +16,7 @@ public interface IPreviewPremiumTaxCommand
|
||||
|
||||
public class PreviewPremiumTaxCommand(
|
||||
ILogger<PreviewPremiumTaxCommand> logger,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter) : BaseBillingCommand<PreviewPremiumTaxCommand>(logger), IPreviewPremiumTaxCommand
|
||||
{
|
||||
public Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(
|
||||
@@ -25,6 +24,8 @@ public class PreviewPremiumTaxCommand(
|
||||
BillingAddress billingAddress)
|
||||
=> HandleAsync<(decimal, decimal)>(async () =>
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true },
|
||||
@@ -41,7 +42,7 @@ public class PreviewPremiumTaxCommand(
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new InvoiceSubscriptionDetailsItemOptions { Price = Prices.PremiumAnnually, Quantity = 1 }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Price = premiumPlan.Seat.StripePriceId, Quantity = 1 }
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -50,7 +51,7 @@ public class PreviewPremiumTaxCommand(
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Price = Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = additionalStorage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
using OrganizationPlan = Plan;
|
||||
using PremiumPlan = Premium.Plan;
|
||||
|
||||
public interface IPricingClient
|
||||
{
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <summary>
|
||||
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
@@ -16,8 +18,9 @@ public interface IPricingClient
|
||||
/// <param name="planType">The type of plan to retrieve.</param>
|
||||
/// <returns>A Bitwarden <see cref="Plan"/> record or null in the case the plan could not be found or the method was executed from a self-hosted instance.</returns>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<Plan?> GetPlan(PlanType planType);
|
||||
Task<OrganizationPlan?> GetPlan(PlanType planType);
|
||||
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <summary>
|
||||
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
@@ -26,13 +29,17 @@ public interface IPricingClient
|
||||
/// <returns>A Bitwarden <see cref="Plan"/> record.</returns>
|
||||
/// <exception cref="NotFoundException">Thrown when the <see cref="Plan"/> for the provided <paramref name="planType"/> could not be found or the method was executed from a self-hosted instance.</exception>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<Plan> GetPlanOrThrow(PlanType planType);
|
||||
Task<OrganizationPlan> GetPlanOrThrow(PlanType planType);
|
||||
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <summary>
|
||||
/// Retrieve all the Bitwarden plans. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
/// </summary>
|
||||
/// <returns>A list of Bitwarden <see cref="Plan"/> records or an empty list in the case the method is executed from a self-hosted instance.</returns>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<List<Plan>> ListPlans();
|
||||
Task<List<OrganizationPlan>> ListPlans();
|
||||
|
||||
Task<PremiumPlan> GetAvailablePremiumPlan();
|
||||
Task<List<PremiumPlan>> ListPremiumPlans();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public class Feature
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public class Plan
|
||||
{
|
||||
@@ -1,8 +1,6 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing.Models;
|
||||
using Plan = Bit.Core.Billing.Pricing.Models.Plan;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public record PlanAdapter : Core.Models.StaticStore.Plan
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
[JsonConverter(typeof(PurchasableJsonConverter))]
|
||||
public class Purchasable(OneOf<Free, Packaged, Scalable> input) : OneOfBase<Free, Packaged, Scalable>(input)
|
||||
10
src/Core/Billing/Pricing/Premium/Plan.cs
Normal file
10
src/Core/Billing/Pricing/Premium/Plan.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Billing.Pricing.Premium;
|
||||
|
||||
public class Plan
|
||||
{
|
||||
public string Name { get; init; } = null!;
|
||||
public int? LegacyYear { get; init; }
|
||||
public bool Available { get; init; }
|
||||
public Purchasable Seat { get; init; } = null!;
|
||||
public Purchasable Storage { get; init; } = null!;
|
||||
}
|
||||
7
src/Core/Billing/Pricing/Premium/Purchasable.cs
Normal file
7
src/Core/Billing/Pricing/Premium/Purchasable.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Billing.Pricing.Premium;
|
||||
|
||||
public class Purchasable
|
||||
{
|
||||
public string StripePriceId { get; init; } = null!;
|
||||
public decimal Price { get; init; }
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing.Organizations;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Plan = Bit.Core.Models.StaticStore.Plan;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
using OrganizationPlan = Bit.Core.Models.StaticStore.Plan;
|
||||
using PremiumPlan = Premium.Plan;
|
||||
using Purchasable = Premium.Purchasable;
|
||||
|
||||
public class PricingClient(
|
||||
IFeatureService featureService,
|
||||
GlobalSettings globalSettings,
|
||||
HttpClient httpClient,
|
||||
ILogger<PricingClient> logger) : IPricingClient
|
||||
{
|
||||
public async Task<Plan?> GetPlan(PlanType planType)
|
||||
public async Task<OrganizationPlan?> GetPlan(PlanType planType)
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
@@ -40,16 +43,14 @@ public class PricingClient(
|
||||
return null;
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}");
|
||||
var response = await httpClient.GetAsync($"plans/organization/{lookupKey}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plan = await response.Content.ReadFromJsonAsync<Models.Plan>();
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
return new PlanAdapter(plan);
|
||||
var plan = await response.Content.ReadFromJsonAsync<Plan>();
|
||||
return plan == null
|
||||
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
|
||||
: new PlanAdapter(plan);
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
@@ -62,19 +63,14 @@ public class PricingClient(
|
||||
message: $"Request to the Pricing Service failed with status code {response.StatusCode}");
|
||||
}
|
||||
|
||||
public async Task<Plan> GetPlanOrThrow(PlanType planType)
|
||||
public async Task<OrganizationPlan> GetPlanOrThrow(PlanType planType)
|
||||
{
|
||||
var plan = await GetPlan(planType);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return plan;
|
||||
return plan ?? throw new NotFoundException($"Could not find plan for type {planType}");
|
||||
}
|
||||
|
||||
public async Task<List<Plan>> ListPlans()
|
||||
public async Task<List<OrganizationPlan>> ListPlans()
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
@@ -88,16 +84,51 @@ public class PricingClient(
|
||||
return StaticStore.Plans.ToList();
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync("plans");
|
||||
var response = await httpClient.GetAsync("plans/organization");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plans = await response.Content.ReadFromJsonAsync<List<Models.Plan>>();
|
||||
if (plans == null)
|
||||
{
|
||||
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
return plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
|
||||
var plans = await response.Content.ReadFromJsonAsync<List<Plan>>();
|
||||
return plans == null
|
||||
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
|
||||
: plans.Select(OrganizationPlan (plan) => new PlanAdapter(plan)).ToList();
|
||||
}
|
||||
|
||||
throw new BillingException(
|
||||
message: $"Request to the Pricing Service failed with status {response.StatusCode}");
|
||||
}
|
||||
|
||||
public async Task<PremiumPlan> GetAvailablePremiumPlan()
|
||||
{
|
||||
var premiumPlans = await ListPremiumPlans();
|
||||
|
||||
var availablePlan = premiumPlans.FirstOrDefault(premiumPlan => premiumPlan.Available);
|
||||
|
||||
return availablePlan ?? throw new NotFoundException("Could not find available premium plan");
|
||||
}
|
||||
|
||||
public async Task<List<PremiumPlan>> ListPremiumPlans()
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||
var fetchPremiumPriceFromPricingService =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService);
|
||||
|
||||
if (!usePricingService || !fetchPremiumPriceFromPricingService)
|
||||
{
|
||||
return [CurrentPremiumPlan];
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync("plans/premium");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plans = await response.Content.ReadFromJsonAsync<List<PremiumPlan>>();
|
||||
return plans ?? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
|
||||
throw new BillingException(
|
||||
@@ -130,4 +161,13 @@ public class PricingClient(
|
||||
PlanType.TeamsStarter2023 => "teams-starter-2023",
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static PremiumPlan CurrentPremiumPlan => new()
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
|
||||
Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -30,7 +31,8 @@ public class PremiumUserBillingService(
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
IUserRepository userRepository) : IPremiumUserBillingService
|
||||
IUserRepository userRepository,
|
||||
IPricingClient pricingClient) : IPremiumUserBillingService
|
||||
{
|
||||
public async Task Credit(User user, decimal amount)
|
||||
{
|
||||
@@ -301,11 +303,13 @@ public class PremiumUserBillingService(
|
||||
Customer customer,
|
||||
int? storage)
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Price = StripeConstants.Prices.PremiumAnnually,
|
||||
Price = premiumPlan.Seat.StripePriceId,
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
@@ -314,7 +318,7 @@ public class PremiumUserBillingService(
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = StripeConstants.Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = storage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ public static class FeatureFlagKeys
|
||||
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
|
||||
public const string PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword =
|
||||
"pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password";
|
||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||
|
||||
/* Autofill Team */
|
||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||
@@ -185,6 +186,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button";
|
||||
public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog";
|
||||
public const string PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page";
|
||||
public const string PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
|
||||
@@ -0,0 +1,675 @@
|
||||
<!doctype html>
|
||||
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<title></title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a { padding:0; }
|
||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||
p { display:block;margin:13px 0; }
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
.mj-column-per-90 { width:90% !important; max-width: 90%; }
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
.moz-text-html .mj-column-per-90 { width:90% !important; max-width: 90%; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
|
||||
@media only screen and (max-width:479px) {
|
||||
table.mj-full-width-mobile { width: 100% !important; }
|
||||
td.mj-full-width-mobile { width: auto !important; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style type="text/css">
|
||||
.border-fix > table {
|
||||
border-collapse: separate !important;
|
||||
}
|
||||
.border-fix > table > tbody > tr > td {
|
||||
border-radius: 3px;
|
||||
}
|
||||
@media only screen and
|
||||
(max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } }
|
||||
|
||||
</style>
|
||||
<!-- Responsive icon visibility -->
|
||||
</head>
|
||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||
|
||||
|
||||
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
|
||||
<!-- Blue Header Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div class="border-fix" style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:434px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:150px;">
|
||||
|
||||
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;"><h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
Verify your email to access this Bitwarden Send
|
||||
</h1></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td><td class="" style="vertical-align:bottom;width:186px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:bottom;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:140px;">
|
||||
|
||||
<img alt src="https://assets.bitwarden.com/email/v1/spot-secure-send-round.png" style="border:0;display:block;outline:none;text-decoration:none;height:140px;width:100%;font-size:16px;" width="140" height="140">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Main Content -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align:top;padding:0px;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">Your verification code is:</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:32px;line-height:1;text-align:left;color:#1B2029;"><b>{{Token}}</b></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="font-size:0px;word-break:break-word;">
|
||||
|
||||
<div style="height:20px;line-height:20px;"> </div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">This code expires in {{Expiry}} minutes. After that, you'll need to
|
||||
verify your email again.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 0px 20px 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="send-bubble-outlook" style="vertical-align:top;width:558px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-90 mj-outlook-group-fix send-bubble" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align:top;padding:0px;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:#DBE5F6;border-radius:16px;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;"><p>
|
||||
Bitwarden Send transmits sensitive, temporary information to
|
||||
others easily and securely. Learn more about
|
||||
<a href="https://bitwarden.com/help/send" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">Bitwarden Send</a>
|
||||
or
|
||||
<a href="https://bitwarden.com/signup" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">sign up</a>
|
||||
to try it today.
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Learn More Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#f6f6f6" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#f6f6f6;background-color:#f6f6f6;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f6f6f6;background-color:#f6f6f6;width:100%;border-radius:0px 0px 4px 4px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:406px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><h3 style="font-size: 20px; margin: 0; line-height: 28px">
|
||||
Learn more about Bitwarden
|
||||
</h3>
|
||||
Find user guides, product documentation, and videos on the
|
||||
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:174px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:94px;">
|
||||
|
||||
<img alt src="https://assets.bitwarden.com/email/v1/spot-community.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="94" height="auto">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:660px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://x.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-x.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-reddit.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://community.bitwarden.com/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-discourse.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://github.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-github.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-youtube.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-facebook.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px">
|
||||
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||
Barbara, CA, USA
|
||||
</p>
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br>
|
||||
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Verify your email to access this Bitwarden Send.
|
||||
|
||||
Your verification code is: {{Token}}
|
||||
|
||||
This code can only be used once and expires in {{Expiry}} minutes. After that you'll need to verify your email again.
|
||||
|
||||
Bitwarden Send transmits sensitive, temporary information to others easily and securely. Learn more about Bitwarden Send or sign up to try it today.
|
||||
{{/BasicTextLayout}}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"packages": [
|
||||
"components/hero"
|
||||
"components/mj-bw-hero"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,38 +2,38 @@
|
||||
<mj-column>
|
||||
<mj-social icon-size="30px" inner-padding="10px" padding="0">
|
||||
<mj-social-element
|
||||
href="https://twitter.com/bitwarden"
|
||||
src="https://bitwarden.com/images/mail-twitter.png"
|
||||
href="https://x.com/bitwarden"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-x.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://www.reddit.com/r/Bitwarden/"
|
||||
src="https://bitwarden.com/images/mail-reddit.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-reddit.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://community.bitwarden.com/"
|
||||
src="https://bitwarden.com/images/mail-discourse.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-discourse.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://github.com/bitwarden"
|
||||
src="https://bitwarden.com/images/mail-github.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-github.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw"
|
||||
src="https://bitwarden.com/images/mail-youtube.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-youtube.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://www.linkedin.com/company/bitwarden1/"
|
||||
src="https://bitwarden.com/images/mail-linkedin.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-linkedin.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://www.facebook.com/bitwarden/"
|
||||
src="https://bitwarden.com/images/mail-facebook.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-facebook.png"
|
||||
></mj-social-element>
|
||||
</mj-social>
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br />
|
||||
<a href="#">bitwarden.com</a> |
|
||||
<a href="#">Learn why we include this</a>
|
||||
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||
</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
font-size="16px"
|
||||
/>
|
||||
<mj-button background-color="#175ddc" />
|
||||
<mj-text color="#333" />
|
||||
<mj-text color="#1B2029" />
|
||||
<mj-body background-color="#e6e9ef" width="660px" />
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
@@ -22,3 +22,9 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
</mj-style>
|
||||
|
||||
<!-- Responsive icon visibility -->
|
||||
<mj-style>
|
||||
@media only screen and
|
||||
(max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } }
|
||||
</mj-style>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
const { BodyComponent } = require("mjml-core");
|
||||
class MjBwHero extends BodyComponent {
|
||||
static dependencies = {
|
||||
// Tell the validator which tags are allowed as our component's parent
|
||||
"mj-column": ["mj-bw-hero"],
|
||||
"mj-wrapper": ["mj-bw-hero"],
|
||||
// Tell the validator which tags are allowed as our component's children
|
||||
"mj-bw-hero": [],
|
||||
};
|
||||
|
||||
static allowedAttributes = {
|
||||
"img-src": "string",
|
||||
title: "string",
|
||||
"button-text": "string",
|
||||
"button-url": "string",
|
||||
};
|
||||
|
||||
static defaultAttributes = {};
|
||||
|
||||
render() {
|
||||
return this.renderMJML(`
|
||||
<mj-section
|
||||
full-width="full-width"
|
||||
background-color="#175ddc"
|
||||
border-radius="4px 4px 0 0"
|
||||
>
|
||||
<mj-column width="70%">
|
||||
<mj-image
|
||||
align="left"
|
||||
src="https://bitwarden.com/images/logo-horizontal-white.png"
|
||||
width="150px"
|
||||
height="30px"
|
||||
></mj-image>
|
||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
${this.getAttribute("title")}
|
||||
</h1>
|
||||
</mj-text>
|
||||
<mj-button
|
||||
href="${this.getAttribute("button-url")}"
|
||||
background-color="#fff"
|
||||
color="#1A41AC"
|
||||
border-radius="20px"
|
||||
align="left"
|
||||
>
|
||||
${this.getAttribute("button-text")}
|
||||
</mj-button
|
||||
>
|
||||
</mj-column>
|
||||
<mj-column width="30%" vertical-align="bottom">
|
||||
<mj-image
|
||||
src="${this.getAttribute("img-src")}"
|
||||
alt=""
|
||||
width="140px"
|
||||
height="140px"
|
||||
padding="0"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MjBwHero;
|
||||
@@ -0,0 +1,18 @@
|
||||
<mj-section border-radius="0px 0px 4px 4px" background-color="#f6f6f6" padding="5px 20px 10px 20px">
|
||||
<mj-column width="70%">
|
||||
<mj-text line-height="24px">
|
||||
<h3 style="font-size: 20px; margin: 0; line-height: 28px">
|
||||
Learn more about Bitwarden
|
||||
</h3>
|
||||
Find user guides, product documentation, and videos on the
|
||||
<a href="https://bitwarden.com/help/" class="link"> Bitwarden Help Center</a>.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="30%">
|
||||
<mj-image
|
||||
src="https://assets.bitwarden.com/email/v1/spot-community.png"
|
||||
css-class="hide-small-img"
|
||||
width="94px"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
100
src/Core/MailTemplates/Mjml/components/mj-bw-hero.js
Normal file
100
src/Core/MailTemplates/Mjml/components/mj-bw-hero.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const { BodyComponent } = require("mjml-core");
|
||||
class MjBwHero extends BodyComponent {
|
||||
static dependencies = {
|
||||
// Tell the validator which tags are allowed as our component's parent
|
||||
"mj-column": ["mj-bw-hero"],
|
||||
"mj-wrapper": ["mj-bw-hero"],
|
||||
// Tell the validator which tags are allowed as our component's children
|
||||
"mj-bw-hero": [],
|
||||
};
|
||||
|
||||
static allowedAttributes = {
|
||||
"img-src": "string", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area
|
||||
title: "string", // REQUIRED: large text stating primary purpose of the email
|
||||
"button-text": "string", // OPTIONAL: text to display in the button
|
||||
"button-url": "string", // OPTIONAL: URL to navigate to when the button is clicked
|
||||
"sub-title": "string", // OPTIONAL: smaller text providing additional context for the title
|
||||
};
|
||||
|
||||
static defaultAttributes = {};
|
||||
|
||||
render() {
|
||||
if (this.getAttribute("button-text") && this.getAttribute("button-url")) {
|
||||
return this.renderMJML(`
|
||||
<mj-section
|
||||
full-width="full-width"
|
||||
background-color="#175ddc"
|
||||
border-radius="4px 4px 0px 0px"
|
||||
>
|
||||
<mj-column width="70%">
|
||||
<mj-image
|
||||
align="left"
|
||||
src="https://bitwarden.com/images/logo-horizontal-white.png"
|
||||
width="150px"
|
||||
height="30px"
|
||||
></mj-image>
|
||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
${this.getAttribute("title")}
|
||||
</h1>
|
||||
</mj-text>
|
||||
<mj-button
|
||||
href="${this.getAttribute("button-url")}"
|
||||
background-color="#fff"
|
||||
color="#1A41AC"
|
||||
border-radius="20px"
|
||||
align="left"
|
||||
>
|
||||
${this.getAttribute("button-text")}
|
||||
</mj-button
|
||||
>
|
||||
</mj-column>
|
||||
<mj-column width="30%" vertical-align="bottom">
|
||||
<mj-image
|
||||
src="${this.getAttribute("img-src")}"
|
||||
alt=""
|
||||
width="140px"
|
||||
height="140px"
|
||||
padding="0px"
|
||||
css-class="hide-small-img"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
`);
|
||||
} else {
|
||||
return this.renderMJML(`
|
||||
<mj-section
|
||||
full-width="full-width"
|
||||
background-color="#175ddc"
|
||||
border-radius="4px 4px 0px 0px"
|
||||
>
|
||||
<mj-column width="70%">
|
||||
<mj-image
|
||||
align="left"
|
||||
src="https://bitwarden.com/images/logo-horizontal-white.png"
|
||||
width="150px"
|
||||
height="30px"
|
||||
></mj-image>
|
||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
${this.getAttribute("title")}
|
||||
</h1>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="30%" vertical-align="bottom">
|
||||
<mj-image
|
||||
src="${this.getAttribute("img-src")}"
|
||||
alt=""
|
||||
width="140px"
|
||||
height="140px"
|
||||
padding="0px"
|
||||
css-class="hide-small-img"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MjBwHero;
|
||||
64
src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml
Normal file
64
src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml
Normal file
@@ -0,0 +1,64 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="../../components/head.mjml" />
|
||||
<mj-style> </mj-style>
|
||||
</mj-head>
|
||||
|
||||
<mj-body css-class="border-fix">
|
||||
<!-- Blue Header Section -->
|
||||
<mj-wrapper css-class="border-fix" padding="20px 20px 10px 20px">
|
||||
<mj-bw-hero
|
||||
img-src="https://assets.bitwarden.com/email/v1/spot-secure-send-round.png"
|
||||
title="Verify your email to access this Bitwarden Send"
|
||||
/>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Main Content -->
|
||||
<mj-wrapper padding="5px 20px 10px 20px">
|
||||
<mj-section background-color="#fff">
|
||||
<mj-column padding="0px">
|
||||
<mj-text> Your verification code is: </mj-text>
|
||||
<mj-text font-size="32px"> <b>{{Token}}</b> </mj-text>
|
||||
<mj-spacer height="20px" />
|
||||
<mj-text>
|
||||
This code expires in {{Expiry}} minutes. After that, you'll need to
|
||||
verify your email again.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section
|
||||
background-color="#fff"
|
||||
padding="0px 0px 20px 0px"
|
||||
>
|
||||
<mj-column
|
||||
css-class="send-bubble"
|
||||
width="90%"
|
||||
inner-background-color="#DBE5F6"
|
||||
inner-border-radius="16px"
|
||||
padding="0px"
|
||||
>
|
||||
<mj-text>
|
||||
<p>
|
||||
Bitwarden Send transmits sensitive, temporary information to
|
||||
others easily and securely. Learn more about
|
||||
<a href="https://bitwarden.com/help/send" class="link"
|
||||
>Bitwarden Send</a
|
||||
>
|
||||
or
|
||||
<a href="https://bitwarden.com/signup" class="link">sign up</a>
|
||||
to try it today.
|
||||
</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Learn More Section -->
|
||||
<mj-wrapper padding="5px 20px 10px 20px">
|
||||
<mj-include path="../../components/learn-more-footer.mjml" />
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Footer -->
|
||||
<mj-include path="../../components/footer.mjml" />
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -1,10 +1,10 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="../components/head.mjml" />
|
||||
<mj-include path="../../components/head.mjml" />
|
||||
</mj-head>
|
||||
|
||||
<mj-body background-color="#f6f6f6">
|
||||
<mj-include path="../components/logo.mjml" />
|
||||
<mj-include path="../../components/logo.mjml" />
|
||||
|
||||
<mj-wrapper
|
||||
background-color="#fff"
|
||||
@@ -24,6 +24,6 @@
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-include path="../components/footer.mjml" />
|
||||
<mj-include path="../../components/footer.mjml" />
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -22,26 +22,7 @@
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#fbfbfb">
|
||||
<mj-column width="70%">
|
||||
<mj-text line-height="24px">
|
||||
<h3 style="font-size: 20px; margin: 0; line-height: 28px">
|
||||
We’re here for you!
|
||||
</h3>
|
||||
If you have any questions, search the Bitwarden
|
||||
<a href="#" class="link">Help</a>
|
||||
site or
|
||||
<a href="#" class="link">contact us</a>.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="30%">
|
||||
<mj-image
|
||||
src="https://assets.bitwarden.com/email/v1/chat.png"
|
||||
height="77px"
|
||||
width="94px"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-include path="../components/learn-more-footer.mjml" />
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-include path="../components/footer.mjml" />
|
||||
|
||||
@@ -9,4 +9,5 @@ public class DefaultEmailOtpViewModel : BaseMailModel
|
||||
public string? TheDate { get; set; }
|
||||
public string? TheTime { get; set; }
|
||||
public string? TimeZone { get; set; }
|
||||
public string? Expiry { get; set; }
|
||||
}
|
||||
|
||||
@@ -226,7 +226,11 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
// Check minimum seats currently in use by the organization
|
||||
if (organization.SmSeats.Value > update.SmSeats.Value)
|
||||
{
|
||||
// Retrieve the number of currently occupied Secrets Manager seats for the organization.
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// Check if the occupied number of seats exceeds the updated seat count.
|
||||
// If so, throw an exception indicating that the subscription cannot be decreased below the current usage.
|
||||
if (occupiedSeats > update.SmSeats.Value)
|
||||
{
|
||||
throw new BadRequestException($"{occupiedSeats} users are currently occupying Secrets Manager seats. " +
|
||||
@@ -412,7 +416,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the number of Secret Manager seats and service accounts are currently used by the organization
|
||||
/// Requests the number of Secret Manager seats and service accounts currently used by the organization
|
||||
/// </summary>
|
||||
/// <param name="organizationId"> The id of the organization</param>
|
||||
/// <returns > A tuple containing the occupied seats and the occupied service account counts</returns>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
@@ -31,6 +32,16 @@ public interface IMailService
|
||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
|
||||
Task SendSendEmailOtpEmailAsync(string email, string token, string subject);
|
||||
/// <summary>
|
||||
/// <see cref="DefaultOtpTokenProviderOptions"/> has a default expiry of 5 minutes so we set the expiry to that value int he view model.
|
||||
/// Sends OTP code token to the specified email address.
|
||||
/// will replace <see cref="SendSendEmailOtpEmailAsync"/> when MJML templates are fully accepted.
|
||||
/// </summary>
|
||||
/// <param name="email">Email address to send the OTP to</param>
|
||||
/// <param name="token">Otp code token</param>
|
||||
/// <param name="subject">subject line of the email</param>
|
||||
/// <returns>Task</returns>
|
||||
Task SendSendEmailOtpEmailv2Async(string email, string token, string subject);
|
||||
Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);
|
||||
Task SendNoMasterPasswordHintEmailAsync(string email);
|
||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||
|
||||
@@ -224,6 +224,27 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
|
||||
{
|
||||
var message = CreateDefaultMessage(subject, email);
|
||||
var requestDateTime = DateTime.UtcNow;
|
||||
var model = new DefaultEmailOtpViewModel
|
||||
{
|
||||
Token = token,
|
||||
Expiry = "5", // This should be configured through the OTPDefaultTokenProviderOptions but for now we will hardcode it to 5 minutes.
|
||||
TheDate = requestDateTime.ToLongDateString(),
|
||||
TheTime = requestDateTime.ToShortTimeString(),
|
||||
TimeZone = _utcTimeZoneDisplay,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
};
|
||||
await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmailv2", model);
|
||||
message.MetaData.Add("SendGridBypassListManagement", true);
|
||||
// TODO - PM-25380 change to string constant
|
||||
message.Category = "SendEmailOtp";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
|
||||
{
|
||||
// Check if we've sent this email within the last hour
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
@@ -896,11 +897,14 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete($"Use {nameof(PreviewPremiumTaxCommand)} instead.")]
|
||||
public async Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(
|
||||
PreviewIndividualInvoiceRequestBody parameters,
|
||||
string gatewayCustomerId,
|
||||
string gatewaySubscriptionId)
|
||||
{
|
||||
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true, },
|
||||
@@ -909,8 +913,17 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = StripeConstants.Prices.PremiumAnnually },
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.AdditionalStorage, Plan = StripeConstants.Prices.StoragePlanPersonal }
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = 1,
|
||||
Plan = premiumPlan.Seat.StripePriceId
|
||||
},
|
||||
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||
Plan = premiumPlan.Storage.StripePriceId
|
||||
}
|
||||
]
|
||||
},
|
||||
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||
@@ -1028,7 +1041,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new()
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||
Plan = plan.PasswordManager.StripeStoragePlanId
|
||||
@@ -1049,7 +1062,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value);
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||
);
|
||||
}
|
||||
else
|
||||
@@ -1057,13 +1070,13 @@ public class StripePaymentService : IPaymentService
|
||||
if (plan.PasswordManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1071,7 +1084,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new()
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.SecretsManager?.Seats ?? 0,
|
||||
Plan = plan.SecretsManager.StripeSeatPlanId
|
||||
@@ -1080,7 +1093,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
if (plan.SecretsManager.HasAdditionalServiceAccountOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new()
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0,
|
||||
Plan = plan.SecretsManager.StripeServiceAccountPlanId
|
||||
|
||||
@@ -14,10 +14,10 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
@@ -72,6 +72,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
@@ -106,7 +107,8 @@ public class UserService : UserManager<User>, IUserService
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IDistributedCache distributedCache,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IPricingClient pricingClient)
|
||||
: base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
@@ -146,6 +148,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_distributedCache = distributedCache;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||
@@ -972,8 +975,9 @@ public class UserService : UserManager<User>, IUserService
|
||||
throw new BadRequestException("Not a premium user.");
|
||||
}
|
||||
|
||||
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb,
|
||||
StripeConstants.Prices.StoragePlanPersonal);
|
||||
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId);
|
||||
await SaveUserAsync(user);
|
||||
return secret;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,11 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
||||
Reference in New Issue
Block a user