mirror of
https://github.com/bitwarden/server
synced 2025-12-30 07:03:42 +00:00
Merge branch 'main' into SM-1571-DisableSMAdsForUsers
This commit is contained in:
@@ -23,7 +23,17 @@ public class IntegrationTemplateContext(EventMessage eventMessage)
|
||||
public Guid? CollectionId => Event.CollectionId;
|
||||
public Guid? GroupId => Event.GroupId;
|
||||
public Guid? PolicyId => Event.PolicyId;
|
||||
public Guid? IdempotencyId => Event.IdempotencyId;
|
||||
public Guid? ProviderId => Event.ProviderId;
|
||||
public Guid? ProviderUserId => Event.ProviderUserId;
|
||||
public Guid? ProviderOrganizationId => Event.ProviderOrganizationId;
|
||||
public Guid? InstallationId => Event.InstallationId;
|
||||
public Guid? SecretId => Event.SecretId;
|
||||
public Guid? ProjectId => Event.ProjectId;
|
||||
public Guid? ServiceAccountId => Event.ServiceAccountId;
|
||||
public Guid? GrantedServiceAccountId => Event.GrantedServiceAccountId;
|
||||
|
||||
public string DateIso8601 => Date.ToString("o");
|
||||
public string EventMessage => JsonSerializer.Serialize(Event);
|
||||
|
||||
public User? User { get; set; }
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Interface defining common organization details properties shared between
|
||||
/// regular organization users and provider organization users for profile endpoints.
|
||||
/// </summary>
|
||||
public interface IProfileOrganizationDetails
|
||||
{
|
||||
Guid? UserId { get; set; }
|
||||
Guid OrganizationId { get; set; }
|
||||
string Name { get; set; }
|
||||
bool Enabled { get; set; }
|
||||
PlanType PlanType { get; set; }
|
||||
bool UsePolicies { get; set; }
|
||||
bool UseSso { get; set; }
|
||||
bool UseKeyConnector { get; set; }
|
||||
bool UseScim { get; set; }
|
||||
bool UseGroups { get; set; }
|
||||
bool UseDirectory { get; set; }
|
||||
bool UseEvents { get; set; }
|
||||
bool UseTotp { get; set; }
|
||||
bool Use2fa { get; set; }
|
||||
bool UseApi { get; set; }
|
||||
bool UseResetPassword { get; set; }
|
||||
bool SelfHost { get; set; }
|
||||
bool UsersGetPremium { get; set; }
|
||||
bool UseCustomPermissions { get; set; }
|
||||
bool UseSecretsManager { get; set; }
|
||||
int? Seats { get; set; }
|
||||
short? MaxCollections { get; set; }
|
||||
short? MaxStorageGb { get; set; }
|
||||
string? Identifier { get; set; }
|
||||
string? Key { get; set; }
|
||||
string? ResetPasswordKey { get; set; }
|
||||
string? PublicKey { get; set; }
|
||||
string? PrivateKey { get; set; }
|
||||
string? SsoExternalId { get; set; }
|
||||
string? Permissions { get; set; }
|
||||
Guid? ProviderId { get; set; }
|
||||
string? ProviderName { get; set; }
|
||||
ProviderType? ProviderType { get; set; }
|
||||
bool? SsoEnabled { get; set; }
|
||||
string? SsoConfig { get; set; }
|
||||
bool UsePasswordManager { get; set; }
|
||||
bool LimitCollectionCreation { get; set; }
|
||||
bool LimitCollectionDeletion { get; set; }
|
||||
bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
bool UseRiskInsights { get; set; }
|
||||
bool LimitItemDeletion { get; set; }
|
||||
bool UseAdminSponsoredFamilies { get; set; }
|
||||
bool UseOrganizationDomains { get; set; }
|
||||
bool UseAutomaticUserConfirmation { get; set; }
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserOrganizationDetails
|
||||
public class OrganizationUserOrganizationDetails : IProfileOrganizationDetails
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid OrganizationUserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
public bool UseKeyConnector { get; set; }
|
||||
@@ -33,24 +31,24 @@ public class OrganizationUserOrganizationDetails
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string? Key { get; set; }
|
||||
public Enums.OrganizationUserStatusType Status { get; set; }
|
||||
public Enums.OrganizationUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public string SsoExternalId { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
public string ResetPasswordKey { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public string? SsoExternalId { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
public string? Permissions { get; set; }
|
||||
public string? ResetPasswordKey { get; set; }
|
||||
public string? PublicKey { get; set; }
|
||||
public string? PrivateKey { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public string? ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public string FamilySponsorshipFriendlyName { get; set; }
|
||||
public string? FamilySponsorshipFriendlyName { get; set; }
|
||||
public bool? SsoEnabled { get; set; }
|
||||
public string SsoConfig { get; set; }
|
||||
public string? SsoConfig { get; set; }
|
||||
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
|
||||
public DateTime? FamilySponsorshipValidUntil { get; set; }
|
||||
public bool? FamilySponsorshipToDelete { get; set; }
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
|
||||
public class ProviderUserOrganizationDetails
|
||||
public class ProviderUserOrganizationDetails : IProfileOrganizationDetails
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
public bool UseKeyConnector { get; set; }
|
||||
@@ -28,20 +25,22 @@ public class ProviderUserOrganizationDetails
|
||||
public bool SelfHost { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string? Key { get; set; }
|
||||
public ProviderUserStatusType Status { get; set; }
|
||||
public ProviderUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
public string? PublicKey { get; set; }
|
||||
public string? PrivateKey { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
public Guid? ProviderUserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public string? ProviderName { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
@@ -50,6 +49,11 @@ public class ProviderUserOrganizationDetails
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public ProviderType ProviderType { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool? SsoEnabled { get; set; }
|
||||
public string? SsoConfig { get; set; }
|
||||
public string? SsoExternalId { get; set; }
|
||||
public string? Permissions { get; set; }
|
||||
public string? ResetPasswordKey { get; set; }
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
}
|
||||
|
||||
var code = Encoding.UTF8.GetString(cachedValue);
|
||||
var valid = string.Equals(token, code);
|
||||
var valid = CoreHelpers.FixedTimeEquals(token, code);
|
||||
if (valid)
|
||||
{
|
||||
await _distributedCache.RemoveAsync(cacheKey);
|
||||
|
||||
@@ -64,7 +64,7 @@ public class OtpTokenProvider<TOptions>(
|
||||
}
|
||||
|
||||
var code = Encoding.UTF8.GetString(cachedValue);
|
||||
var valid = string.Equals(token, code);
|
||||
var valid = CoreHelpers.FixedTimeEquals(token, code);
|
||||
if (valid)
|
||||
{
|
||||
await _distributedCache.RemoveAsync(cacheKey);
|
||||
|
||||
14
src/Core/Billing/Constants/BitPayConstants.cs
Normal file
14
src/Core/Billing/Constants/BitPayConstants.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Bit.Core.Billing.Constants;
|
||||
|
||||
public static class BitPayConstants
|
||||
{
|
||||
public static class InvoiceStatuses
|
||||
{
|
||||
public const string Complete = "complete";
|
||||
}
|
||||
|
||||
public static class PosDataKeys
|
||||
{
|
||||
public const string AccountCredit = "accountCredit:1";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Clients;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
@@ -9,6 +10,8 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Billing.Payment.Commands;
|
||||
|
||||
using static BitPayConstants;
|
||||
|
||||
public interface ICreateBitPayInvoiceForCreditCommand
|
||||
{
|
||||
Task<BillingCommandResult<string>> Run(
|
||||
@@ -31,6 +34,8 @@ public class CreateBitPayInvoiceForCreditCommand(
|
||||
{
|
||||
var (name, email, posData) = GetSubscriberInformation(subscriber);
|
||||
|
||||
var notificationUrl = $"{globalSettings.BitPay.NotificationUrl}?key={globalSettings.BitPay.WebhookKey}";
|
||||
|
||||
var invoice = new Invoice
|
||||
{
|
||||
Buyer = new Buyer { Email = email, Name = name },
|
||||
@@ -38,7 +43,7 @@ public class CreateBitPayInvoiceForCreditCommand(
|
||||
ExtendedNotifications = true,
|
||||
FullNotifications = true,
|
||||
ItemDesc = "Bitwarden",
|
||||
NotificationUrl = globalSettings.BitPay.NotificationUrl,
|
||||
NotificationUrl = notificationUrl,
|
||||
PosData = posData,
|
||||
Price = Convert.ToDouble(amount),
|
||||
RedirectUrl = redirectUrl
|
||||
@@ -51,10 +56,10 @@ public class CreateBitPayInvoiceForCreditCommand(
|
||||
private static (string? Name, string? Email, string POSData) GetSubscriberInformation(
|
||||
ISubscriber subscriber) => subscriber switch
|
||||
{
|
||||
User user => (user.Email, user.Email, $"userId:{user.Id},accountCredit:1"),
|
||||
User user => (user.Email, user.Email, $"userId:{user.Id},{PosDataKeys.AccountCredit}"),
|
||||
Organization organization => (organization.Name, organization.BillingEmail,
|
||||
$"organizationId:{organization.Id},accountCredit:1"),
|
||||
Provider provider => (provider.Name, provider.BillingEmail, $"providerId:{provider.Id},accountCredit:1"),
|
||||
$"organizationId:{organization.Id},{PosDataKeys.AccountCredit}"),
|
||||
Provider provider => (provider.Name, provider.BillingEmail, $"providerId:{provider.Id},{PosDataKeys.AccountCredit}"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(subscriber))
|
||||
};
|
||||
}
|
||||
|
||||
11
src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs
Normal file
11
src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.Billing.Payment.Models;
|
||||
|
||||
public record NonTokenizedPaymentMethod
|
||||
{
|
||||
public NonTokenizablePaymentMethodType Type { get; set; }
|
||||
}
|
||||
|
||||
public enum NonTokenizablePaymentMethodType
|
||||
{
|
||||
AccountCredit,
|
||||
}
|
||||
69
src/Core/Billing/Payment/Models/PaymentMethod.cs
Normal file
69
src/Core/Billing/Payment/Models/PaymentMethod.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Payment.Models;
|
||||
|
||||
[JsonConverter(typeof(PaymentMethodJsonConverter))]
|
||||
public class PaymentMethod(OneOf<TokenizedPaymentMethod, NonTokenizedPaymentMethod> input)
|
||||
: OneOfBase<TokenizedPaymentMethod, NonTokenizedPaymentMethod>(input)
|
||||
{
|
||||
public static implicit operator PaymentMethod(TokenizedPaymentMethod tokenized) => new(tokenized);
|
||||
public static implicit operator PaymentMethod(NonTokenizedPaymentMethod nonTokenized) => new(nonTokenized);
|
||||
public bool IsTokenized => IsT0;
|
||||
public bool IsNonTokenized => IsT1;
|
||||
}
|
||||
|
||||
internal class PaymentMethodJsonConverter : JsonConverter<PaymentMethod>
|
||||
{
|
||||
public override PaymentMethod Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var element = JsonElement.ParseValue(ref reader);
|
||||
|
||||
if (!element.TryGetProperty("type", out var typeProperty))
|
||||
{
|
||||
throw new JsonException("PaymentMethod requires a 'type' property");
|
||||
}
|
||||
|
||||
var type = typeProperty.GetString();
|
||||
|
||||
|
||||
if (Enum.TryParse<TokenizablePaymentMethodType>(type, true, out var tokenizedType) &&
|
||||
Enum.IsDefined(typeof(TokenizablePaymentMethodType), tokenizedType))
|
||||
{
|
||||
var token = element.TryGetProperty("token", out var tokenProperty) ? tokenProperty.GetString() : null;
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
throw new JsonException("TokenizedPaymentMethod requires a 'token' property");
|
||||
}
|
||||
|
||||
return new TokenizedPaymentMethod { Type = tokenizedType, Token = token };
|
||||
}
|
||||
|
||||
if (Enum.TryParse<NonTokenizablePaymentMethodType>(type, true, out var nonTokenizedType) &&
|
||||
Enum.IsDefined(typeof(NonTokenizablePaymentMethodType), nonTokenizedType))
|
||||
{
|
||||
return new NonTokenizedPaymentMethod { Type = nonTokenizedType };
|
||||
}
|
||||
|
||||
throw new JsonException($"Unknown payment method type: {type}");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, PaymentMethod value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
|
||||
value.Switch(
|
||||
tokenized =>
|
||||
{
|
||||
writer.WriteString("type",
|
||||
tokenized.Type.ToString().ToLowerInvariant()
|
||||
);
|
||||
writer.WriteString("token", tokenized.Token);
|
||||
},
|
||||
nonTokenized => { writer.WriteString("type", nonTokenized.Type.ToString().ToLowerInvariant()); }
|
||||
);
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using Microsoft.Extensions.Logging;
|
||||
using OneOf.Types;
|
||||
using Stripe;
|
||||
using Customer = Stripe.Customer;
|
||||
using PaymentMethod = Bit.Core.Billing.Payment.Models.PaymentMethod;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Core.Billing.Premium.Commands;
|
||||
@@ -38,7 +39,7 @@ public interface ICreatePremiumCloudHostedSubscriptionCommand
|
||||
/// <returns>A billing command result indicating success or failure with appropriate error details.</returns>
|
||||
Task<BillingCommandResult<None>> Run(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
PaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress,
|
||||
short additionalStorageGb);
|
||||
}
|
||||
@@ -60,7 +61,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
|
||||
public Task<BillingCommandResult<None>> Run(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
PaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress,
|
||||
short additionalStorageGb) => HandleAsync<None>(async () =>
|
||||
{
|
||||
@@ -74,6 +75,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
return new BadRequest("Additional storage must be greater than 0.");
|
||||
}
|
||||
|
||||
// Note: A customer will already exist if the customer has purchased account credits.
|
||||
var customer = string.IsNullOrEmpty(user.GatewayCustomerId)
|
||||
? await CreateCustomerAsync(user, paymentMethod, billingAddress)
|
||||
: await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = _expand });
|
||||
@@ -82,18 +84,31 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
|
||||
var subscription = await CreateSubscriptionAsync(user.Id, customer, additionalStorageGb > 0 ? additionalStorageGb : null);
|
||||
|
||||
switch (paymentMethod)
|
||||
{
|
||||
case { Type: TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete:
|
||||
case { Type: not TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == StripeConstants.SubscriptionStatus.Active:
|
||||
paymentMethod.Switch(
|
||||
tokenized =>
|
||||
{
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (tokenized)
|
||||
{
|
||||
case { Type: TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete:
|
||||
case { Type: not TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == StripeConstants.SubscriptionStatus.Active:
|
||||
{
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
nonTokenized =>
|
||||
{
|
||||
if (subscription.Status == StripeConstants.SubscriptionStatus.Active)
|
||||
{
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
user.GatewayCustomerId = customer.Id;
|
||||
@@ -109,9 +124,15 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
});
|
||||
|
||||
private async Task<Customer> CreateCustomerAsync(User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
PaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
if (paymentMethod.IsNonTokenized)
|
||||
{
|
||||
_logger.LogError("Cannot create customer for user ({UserID}) using non-tokenized payment method. The customer should already exist", user.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var subscriberName = user.SubscriberName();
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
@@ -153,13 +174,14 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
|
||||
var braintreeCustomerId = "";
|
||||
|
||||
// We have checked that the payment method is tokenized, so we can safely cast it.
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (paymentMethod.Type)
|
||||
switch (paymentMethod.AsT0.Type)
|
||||
{
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.Token }))
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.AsT0.Token }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (setupIntent == null)
|
||||
@@ -173,19 +195,19 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
}
|
||||
case TokenizablePaymentMethodType.Card:
|
||||
{
|
||||
customerCreateOptions.PaymentMethod = paymentMethod.Token;
|
||||
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token;
|
||||
customerCreateOptions.PaymentMethod = paymentMethod.AsT0.Token;
|
||||
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.AsT0.Token;
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.PayPal:
|
||||
{
|
||||
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.Token);
|
||||
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.AsT0.Token);
|
||||
customerCreateOptions.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
_logger.LogError("Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported", user.Id, paymentMethod.Type.ToString());
|
||||
_logger.LogError("Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported", user.Id, paymentMethod.AsT0.Type.ToString());
|
||||
throw new BillingException();
|
||||
}
|
||||
}
|
||||
@@ -203,18 +225,21 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
async Task Revert()
|
||||
{
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (paymentMethod.Type)
|
||||
if (paymentMethod.IsTokenized)
|
||||
{
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id);
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
switch (paymentMethod.AsT0.Type)
|
||||
{
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id);
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ public static class FeatureFlagKeys
|
||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||
public const string Otp6Digits = "pm-18612-otp-6-digits";
|
||||
public const string FailedTwoFactorEmail = "pm-24425-send-2fa-failed-email";
|
||||
public const string PM24579_PreventSsoOnExistingNonCompliantUsers = "pm-24579-prevent-sso-on-existing-non-compliant-users";
|
||||
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";
|
||||
@@ -241,6 +242,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view";
|
||||
public const string PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp";
|
||||
public const string PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption";
|
||||
public const string PM23904_RiskInsightsForPremium = "pm-23904-risk-insights-for-premium";
|
||||
|
||||
/* Innovation Team */
|
||||
public const string ArchiveVaultItems = "pm-19148-innovation-archive";
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="licensing.cer" />
|
||||
<EmbeddedResource Include="licensing_dev.cer" />
|
||||
<EmbeddedResource Include="MailTemplates\Handlebars\**\*.hbs" />
|
||||
|
||||
<!-- Email templates uses .hbs extension, they must be included for emails to work -->
|
||||
<EmbeddedResource Include="**\*.hbs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -72,7 +74,7 @@
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="Properties\" />
|
||||
|
||||
@@ -11,12 +11,24 @@ public class OrganizationReport : ITableObject<Guid>
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string ReportData { get; set; } = string.Empty;
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public string ContentEncryptionKey { get; set; } = string.Empty;
|
||||
|
||||
public string? SummaryData { get; set; } = null;
|
||||
public string? ApplicationData { get; set; } = null;
|
||||
public string? SummaryData { get; set; }
|
||||
public string? ApplicationData { get; set; }
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
public int? ApplicationCount { get; set; }
|
||||
public int? ApplicationAtRiskCount { get; set; }
|
||||
public int? CriticalApplicationCount { get; set; }
|
||||
public int? CriticalApplicationAtRiskCount { get; set; }
|
||||
public int? MemberCount { get; set; }
|
||||
public int? MemberAtRiskCount { get; set; }
|
||||
public int? CriticalMemberCount { get; set; }
|
||||
public int? CriticalMemberAtRiskCount { get; set; }
|
||||
public int? PasswordCount { get; set; }
|
||||
public int? PasswordAtRiskCount { get; set; }
|
||||
public int? CriticalPasswordCount { get; set; }
|
||||
public int? CriticalPasswordAtRiskCount { get; set; }
|
||||
|
||||
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
|
||||
2373
src/Core/MailTemplates/Mjml/package-lock.json
generated
2373
src/Core/MailTemplates/Mjml/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
||||
"prettier": "prettier --cache --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"mjml": "4.15.3",
|
||||
"mjml": "4.16.1",
|
||||
"mjml-core": "4.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
54
src/Core/Platform/Mailer/BaseMail.cs
Normal file
54
src/Core/Platform/Mailer/BaseMail.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// BaseMail describes a model for emails. It contains metadata about the email such as recipients,
|
||||
/// subject, and an optional category for processing at the upstream email delivery service.
|
||||
///
|
||||
/// Each BaseMail must have a view model that inherits from BaseMailView. The view model is used to
|
||||
/// generate the text part and HTML body.
|
||||
/// </summary>
|
||||
public abstract class BaseMail<TView> where TView : BaseMailView
|
||||
{
|
||||
/// <summary>
|
||||
/// Email recipients.
|
||||
/// </summary>
|
||||
public required IEnumerable<string> ToEmails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The subject of the email.
|
||||
/// </summary>
|
||||
public abstract string Subject { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional category for processing at the upstream email delivery service.
|
||||
/// </summary>
|
||||
public string? Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Allows you to override and ignore the suppression list for this email.
|
||||
///
|
||||
/// Warning: This should be used with caution, valid reasons are primarily account recovery, email OTP.
|
||||
/// </summary>
|
||||
public virtual bool IgnoreSuppressList { get; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// View model for the email body.
|
||||
/// </summary>
|
||||
public required TView View { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Each MailView consists of two body parts: a text part and an HTML part and the filename must be
|
||||
/// relative to the viewmodel and match the following pattern:
|
||||
/// - `{ClassName}.html.hbs` for the HTML part
|
||||
/// - `{ClassName}.text.hbs` for the text part
|
||||
/// </summary>
|
||||
public abstract class BaseMailView
|
||||
{
|
||||
/// <summary>
|
||||
/// Current year.
|
||||
/// </summary>
|
||||
public string CurrentYear => DateTime.UtcNow.Year.ToString();
|
||||
}
|
||||
80
src/Core/Platform/Mailer/HandlebarMailRenderer.cs
Normal file
80
src/Core/Platform/Mailer/HandlebarMailRenderer.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using HandlebarsDotNet;
|
||||
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
public class HandlebarMailRenderer : IMailRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Lazy-initialized Handlebars instance. Thread-safe and ensures initialization occurs only once.
|
||||
/// </summary>
|
||||
private readonly Lazy<Task<IHandlebars>> _handlebarsTask = new(InitializeHandlebarsAsync, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
/// <summary>
|
||||
/// Helper function that returns the handlebar instance.
|
||||
/// </summary>
|
||||
private Task<IHandlebars> GetHandlebars() => _handlebarsTask.Value;
|
||||
|
||||
/// <summary>
|
||||
/// This dictionary is used to cache compiled templates in a thread-safe manner.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, Lazy<Task<HandlebarsTemplate<object, object>>>> _templateCache = new();
|
||||
|
||||
public async Task<(string html, string txt)> RenderAsync(BaseMailView model)
|
||||
{
|
||||
var html = await CompileTemplateAsync(model, "html");
|
||||
var txt = await CompileTemplateAsync(model, "text");
|
||||
|
||||
return (html, txt);
|
||||
}
|
||||
|
||||
private async Task<string> CompileTemplateAsync(BaseMailView model, string type)
|
||||
{
|
||||
var templateName = $"{model.GetType().FullName}.{type}.hbs";
|
||||
var assembly = model.GetType().Assembly;
|
||||
|
||||
// GetOrAdd is atomic - only one Lazy will be stored per templateName.
|
||||
// The Lazy with ExecutionAndPublication ensures the compilation happens exactly once.
|
||||
var lazyTemplate = _templateCache.GetOrAdd(
|
||||
templateName,
|
||||
key => new Lazy<Task<HandlebarsTemplate<object, object>>>(
|
||||
() => CompileTemplateInternalAsync(assembly, key),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication));
|
||||
|
||||
var template = await lazyTemplate.Value;
|
||||
return template(model);
|
||||
}
|
||||
|
||||
private async Task<HandlebarsTemplate<object, object>> CompileTemplateInternalAsync(Assembly assembly, string templateName)
|
||||
{
|
||||
var source = await ReadSourceAsync(assembly, templateName);
|
||||
var handlebars = await GetHandlebars();
|
||||
return handlebars.Compile(source);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadSourceAsync(Assembly assembly, string template)
|
||||
{
|
||||
if (assembly.GetManifestResourceNames().All(f => f != template))
|
||||
{
|
||||
throw new FileNotFoundException("Template not found: " + template);
|
||||
}
|
||||
|
||||
await using var s = assembly.GetManifestResourceStream(template)!;
|
||||
using var sr = new StreamReader(s);
|
||||
return await sr.ReadToEndAsync();
|
||||
}
|
||||
|
||||
private static async Task<IHandlebars> InitializeHandlebarsAsync()
|
||||
{
|
||||
var handlebars = Handlebars.Create();
|
||||
|
||||
// TODO: Do we still need layouts with MJML?
|
||||
var assembly = typeof(HandlebarMailRenderer).Assembly;
|
||||
var layoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.html.hbs");
|
||||
handlebars.RegisterTemplate("FullHtmlLayout", layoutSource);
|
||||
|
||||
return handlebars;
|
||||
}
|
||||
}
|
||||
7
src/Core/Platform/Mailer/IMailRenderer.cs
Normal file
7
src/Core/Platform/Mailer/IMailRenderer.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
#nullable enable
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
public interface IMailRenderer
|
||||
{
|
||||
Task<(string html, string txt)> RenderAsync(BaseMailView model);
|
||||
}
|
||||
15
src/Core/Platform/Mailer/IMailer.cs
Normal file
15
src/Core/Platform/Mailer/IMailer.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Generic mailer interface for sending email messages.
|
||||
/// </summary>
|
||||
public interface IMailer
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends an email message.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public Task SendEmail<T>(BaseMail<T> message) where T : BaseMailView;
|
||||
}
|
||||
32
src/Core/Platform/Mailer/Mailer.cs
Normal file
32
src/Core/Platform/Mailer/Mailer.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class Mailer(IMailRenderer renderer, IMailDeliveryService mailDeliveryService) : IMailer
|
||||
{
|
||||
public async Task SendEmail<T>(BaseMail<T> message) where T : BaseMailView
|
||||
{
|
||||
var content = await renderer.RenderAsync(message.View);
|
||||
|
||||
var metadata = new Dictionary<string, object>();
|
||||
if (message.IgnoreSuppressList)
|
||||
{
|
||||
metadata.Add("SendGridBypassListManagement", true);
|
||||
}
|
||||
|
||||
var mailMessage = new MailMessage
|
||||
{
|
||||
ToEmails = message.ToEmails,
|
||||
Subject = message.Subject,
|
||||
MetaData = metadata,
|
||||
HtmlContent = content.html,
|
||||
TextContent = content.txt,
|
||||
Category = message.Category,
|
||||
};
|
||||
|
||||
await mailDeliveryService.SendEmailAsync(mailMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for adding the Mailer feature to the service collection.
|
||||
/// </summary>
|
||||
public static class MailerServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the Mailer services to the <see cref="IServiceCollection"/>.
|
||||
/// This includes the mail renderer and mailer for sending templated emails.
|
||||
/// This method is safe to be run multiple times.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> for additional chaining.</returns>
|
||||
public static IServiceCollection AddMailer(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<IMailRenderer, HandlebarMailRenderer>();
|
||||
services.TryAddSingleton<IMailer, Mailer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
200
src/Core/Platform/Mailer/README.md
Normal file
200
src/Core/Platform/Mailer/README.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Mailer
|
||||
|
||||
The Mailer feature provides a structured, type-safe approach to sending emails in the Bitwarden server application. It
|
||||
uses Handlebars templates to render both HTML and plain text email content.
|
||||
|
||||
## Architecture
|
||||
|
||||
The Mailer system consists of four main components:
|
||||
|
||||
1. **IMailer** - Service interface for sending emails
|
||||
2. **BaseMail<TView>** - Abstract base class defining email metadata (recipients, subject, category)
|
||||
3. **BaseMailView** - Abstract base class for email template view models
|
||||
4. **IMailRenderer** - Internal interface for rendering templates (implemented by `HandlebarMailRenderer`)
|
||||
|
||||
## How To Use
|
||||
|
||||
1. Define a view model that inherits from `BaseMailView` with properties for template data
|
||||
2. Create Handlebars templates (`.html.hbs` and `.text.hbs`) as embedded resources, preferably using the MJML pipeline,
|
||||
`/src/Core/MailTemplates/Mjml`.
|
||||
3. Define an email class that inherits from `BaseMail<TView>` with metadata like subject
|
||||
4. Use `IMailer.SendEmail()` to render and send the email
|
||||
|
||||
## Creating a New Email
|
||||
|
||||
### Step 1: Define the Email & View Model
|
||||
|
||||
Create a class that inherits from `BaseMailView`:
|
||||
|
||||
```csharp
|
||||
using Bit.Core.Platform.Mailer;
|
||||
|
||||
namespace MyApp.Emails;
|
||||
|
||||
public class WelcomeEmailView : BaseMailView
|
||||
{
|
||||
public required string UserName { get; init; }
|
||||
public required string ActivationUrl { get; init; }
|
||||
}
|
||||
|
||||
public class WelcomeEmail : BaseMail<WelcomeEmailView>
|
||||
{
|
||||
public override string Subject => "Welcome to Bitwarden";
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Create Handlebars Templates
|
||||
|
||||
Create two template files as embedded resources next to your view model. **Important**: The file names must be located
|
||||
directly next to the `ViewClass` and match the name of the view.
|
||||
|
||||
**WelcomeEmailView.html.hbs** (HTML version):
|
||||
|
||||
```handlebars
|
||||
<h1>Welcome, {{ UserName }}!</h1>
|
||||
<p>Thank you for joining Bitwarden.</p>
|
||||
<p>
|
||||
<a href="{{ ActivationUrl }}">Activate your account</a>
|
||||
</p>
|
||||
<p><small>© {{ CurrentYear }} Bitwarden Inc.</small></p>
|
||||
```
|
||||
|
||||
**WelcomeEmailView.text.hbs** (plain text version):
|
||||
|
||||
```handlebars
|
||||
Welcome, {{ UserName }}!
|
||||
|
||||
Thank you for joining Bitwarden.
|
||||
|
||||
Activate your account: {{ ActivationUrl }}
|
||||
|
||||
<EFBFBD> {{ CurrentYear }} Bitwarden Inc.
|
||||
```
|
||||
|
||||
**Important**: Template files must be configured as embedded resources in your `.csproj`:
|
||||
|
||||
```xml
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="**\*.hbs" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### Step 3: Send the Email
|
||||
|
||||
Inject `IMailer` and send the email, this may be done in a service, command or some other application layer.
|
||||
|
||||
```csharp
|
||||
public class SomeService
|
||||
{
|
||||
private readonly IMailer _mailer;
|
||||
|
||||
public SomeService(IMailer mailer)
|
||||
{
|
||||
_mailer = mailer;
|
||||
}
|
||||
|
||||
public async Task SendWelcomeEmailAsync(string email, string userName, string activationUrl)
|
||||
{
|
||||
var mail = new WelcomeEmail
|
||||
{
|
||||
ToEmails = [email],
|
||||
View = new WelcomeEmailView
|
||||
{
|
||||
UserName = userName,
|
||||
ActivationUrl = activationUrl
|
||||
}
|
||||
};
|
||||
|
||||
await _mailer.SendEmail(mail);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Multiple Recipients
|
||||
|
||||
Send to multiple recipients by providing multiple email addresses:
|
||||
|
||||
```csharp
|
||||
var mail = new WelcomeEmail
|
||||
{
|
||||
ToEmails = ["user1@example.com", "user2@example.com"],
|
||||
View = new WelcomeEmailView { /* ... */ }
|
||||
};
|
||||
```
|
||||
|
||||
### Bypass Suppression List
|
||||
|
||||
For critical emails like account recovery or email OTP, you can bypass the suppression list:
|
||||
|
||||
```csharp
|
||||
public class PasswordResetEmail : BaseMail<PasswordResetEmailView>
|
||||
{
|
||||
public override string Subject => "Reset Your Password";
|
||||
public override bool IgnoreSuppressList => true; // Use with caution
|
||||
}
|
||||
```
|
||||
|
||||
**Warning**: Only use `IgnoreSuppressList = true` for critical account recovery or authentication emails.
|
||||
|
||||
### Email Categories
|
||||
|
||||
Optionally categorize emails for processing at the upstream email delivery service:
|
||||
|
||||
```csharp
|
||||
public class MarketingEmail : BaseMail<MarketingEmailView>
|
||||
{
|
||||
public override string Subject => "Latest Updates";
|
||||
public string? Category => "marketing";
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in View Properties
|
||||
|
||||
All view models inherit from `BaseMailView`, which provides:
|
||||
|
||||
- **CurrentYear** - The current UTC year (useful for copyright notices)
|
||||
|
||||
```handlebars
|
||||
|
||||
<footer>© {{ CurrentYear }} Bitwarden Inc.</footer>
|
||||
```
|
||||
|
||||
## Template Naming Convention
|
||||
|
||||
Templates must follow this naming convention:
|
||||
|
||||
- HTML template: `{ViewModelFullName}.html.hbs`
|
||||
- Text template: `{ViewModelFullName}.text.hbs`
|
||||
|
||||
For example, if your view model is `Bit.Core.Auth.Models.Mail.VerifyEmailView`, the templates must be:
|
||||
|
||||
- `Bit.Core.Auth.Models.Mail.VerifyEmailView.html.hbs`
|
||||
- `Bit.Core.Auth.Models.Mail.VerifyEmailView.text.hbs`
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Register the Mailer services in your DI container using the extension method:
|
||||
|
||||
```csharp
|
||||
using Bit.Core.Platform.Mailer;
|
||||
|
||||
services.AddMailer();
|
||||
```
|
||||
|
||||
Or manually register the services:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
services.TryAddSingleton<IMailRenderer, HandlebarMailRenderer>();
|
||||
services.TryAddSingleton<IMailer, Mailer>();
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- **Template caching** - `HandlebarMailRenderer` automatically caches compiled templates
|
||||
- **Lazy initialization** - Handlebars is initialized only when first needed
|
||||
- **Thread-safe** - The renderer is thread-safe for concurrent email rendering
|
||||
@@ -508,9 +508,15 @@
|
||||
<data name="UserIdAndTokenMismatch" xml:space="preserve">
|
||||
<value>Supplied userId and token did not match.</value>
|
||||
</data>
|
||||
<data name="UserShouldBeFound" xml:space="preserve">
|
||||
<value>User should have been defined by this point.</value>
|
||||
</data>
|
||||
<data name="CouldNotFindOrganization" xml:space="preserve">
|
||||
<value>Could not find organization for '{0}'</value>
|
||||
</data>
|
||||
<data name="CouldNotFindOrganizationUser" xml:space="preserve">
|
||||
<value>Could not find organization user for user '{0}' organization '{1}'</value>
|
||||
</data>
|
||||
<data name="NoSeatsAvailable" xml:space="preserve">
|
||||
<value>No seats available for organization, '{0}'</value>
|
||||
</data>
|
||||
|
||||
@@ -677,6 +677,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public bool Production { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public string WebhookKey { get; set; }
|
||||
}
|
||||
|
||||
public class InstallationSettings : IInstallationSettings
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
public class BitPayClient
|
||||
{
|
||||
private readonly BitPayLight.BitPay _bpClient;
|
||||
|
||||
public BitPayClient(GlobalSettings globalSettings)
|
||||
{
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.BitPay.Token))
|
||||
{
|
||||
_bpClient = new BitPayLight.BitPay(globalSettings.BitPay.Token,
|
||||
globalSettings.BitPay.Production ? BitPayLight.Env.Prod : BitPayLight.Env.Test);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<BitPayLight.Models.Invoice.Invoice> GetInvoiceAsync(string id)
|
||||
{
|
||||
return _bpClient.GetInvoice(id);
|
||||
}
|
||||
|
||||
public Task<BitPayLight.Models.Invoice.Invoice> CreateInvoiceAsync(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
return _bpClient.CreateInvoice(invoice);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user