mirror of
https://github.com/bitwarden/server.git
synced 2025-02-23 03:01:23 +01:00
paymentservice with stripe & braintree implem.
This commit is contained in:
parent
c991d48cbc
commit
2dc9c196c4
@ -3,6 +3,9 @@
|
|||||||
"baseVaultUri": "https://vault.bitwarden.com/#",
|
"baseVaultUri": "https://vault.bitwarden.com/#",
|
||||||
"u2f": {
|
"u2f": {
|
||||||
"appId": "https://vault.bitwarden.com/app-id.json"
|
"appId": "https://vault.bitwarden.com/app-id.json"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,12 @@
|
|||||||
},
|
},
|
||||||
"u2f": {
|
"u2f": {
|
||||||
"appId": "https://localhost:4001/app-id.json"
|
"appId": "https://localhost:4001/app-id.json"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": false,
|
||||||
|
"merchantId": "SECRET",
|
||||||
|
"publicKey": "SECRET",
|
||||||
|
"privateKey": "SECRET"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"IpRateLimitOptions": {
|
"IpRateLimitOptions": {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"globalSettings": {
|
"globalSettings": {
|
||||||
"baseVaultUri": "https://vault.bitwarden.com/#"
|
"baseVaultUri": "https://vault.bitwarden.com/#",
|
||||||
|
"braintree": {
|
||||||
|
"production": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,5 +30,11 @@
|
|||||||
},
|
},
|
||||||
"billingSettings": {
|
"billingSettings": {
|
||||||
"stripeWebhookKey": "SECRET"
|
"stripeWebhookKey": "SECRET"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": false,
|
||||||
|
"merchantId": "SECRET",
|
||||||
|
"publicKey": "SECRET",
|
||||||
|
"privateKey": "SECRET"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Braintree" Version="3.8.0" />
|
||||||
<PackageReference Include="IdentityServer4" Version="1.3.1" />
|
<PackageReference Include="IdentityServer4" Version="1.3.1" />
|
||||||
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="1.0.1" />
|
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="1.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.AzureStorage" Version="1.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.AzureStorage" Version="1.0.2" />
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
||||||
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
||||||
public virtual U2fSettings U2f { get; set; } = new U2fSettings();
|
public virtual U2fSettings U2f { get; set; } = new U2fSettings();
|
||||||
|
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
|
||||||
|
|
||||||
public class SqlServerSettings
|
public class SqlServerSettings
|
||||||
{
|
{
|
||||||
@ -86,5 +87,13 @@
|
|||||||
{
|
{
|
||||||
public string AppId { get; set; }
|
public string AppId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class BraintreeSettings
|
||||||
|
{
|
||||||
|
public bool Production { get; set; }
|
||||||
|
public string MerchantId { get; set; }
|
||||||
|
public string PublicKey { get; set; }
|
||||||
|
public string PrivateKey { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
src/Core/Services/IPaymentService.cs
Normal file
10
src/Core/Services/IPaymentService.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services
|
||||||
|
{
|
||||||
|
public interface IPaymentService
|
||||||
|
{
|
||||||
|
Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb);
|
||||||
|
}
|
||||||
|
}
|
72
src/Core/Services/Implementations/BraintreePaymentService.cs
Normal file
72
src/Core/Services/Implementations/BraintreePaymentService.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using Braintree;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services
|
||||||
|
{
|
||||||
|
public class BraintreePaymentService : IPaymentService
|
||||||
|
{
|
||||||
|
private readonly BraintreeGateway _gateway;
|
||||||
|
|
||||||
|
public BraintreePaymentService(
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
_gateway = new BraintreeGateway
|
||||||
|
{
|
||||||
|
Environment = globalSettings.Braintree.Production ?
|
||||||
|
Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,
|
||||||
|
MerchantId = globalSettings.Braintree.MerchantId,
|
||||||
|
PublicKey = globalSettings.Braintree.PublicKey,
|
||||||
|
PrivateKey = globalSettings.Braintree.PrivateKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb)
|
||||||
|
{
|
||||||
|
var customerResult = await _gateway.Customer.CreateAsync(new CustomerRequest
|
||||||
|
{
|
||||||
|
PaymentMethodNonce = paymentToken,
|
||||||
|
Email = user.Email
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!customerResult.IsSuccess())
|
||||||
|
{
|
||||||
|
// error, throw something
|
||||||
|
}
|
||||||
|
|
||||||
|
var subId = "u" + user.Id.ToString("N").ToLower() +
|
||||||
|
Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||||
|
|
||||||
|
var subRequest = new SubscriptionRequest
|
||||||
|
{
|
||||||
|
Id = subId,
|
||||||
|
PaymentMethodToken = paymentToken,
|
||||||
|
PlanId = "premium-annually"
|
||||||
|
};
|
||||||
|
|
||||||
|
if(additionalStorageGb > 0)
|
||||||
|
{
|
||||||
|
subRequest.AddOns.Add = new AddAddOnRequest[]
|
||||||
|
{
|
||||||
|
new AddAddOnRequest
|
||||||
|
{
|
||||||
|
InheritedFromId = "storage-gb-annually",
|
||||||
|
Quantity = additionalStorageGb
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var subResult = await _gateway.Subscription.CreateAsync(subRequest);
|
||||||
|
|
||||||
|
if(!subResult.IsSuccess())
|
||||||
|
{
|
||||||
|
await _gateway.Customer.DeleteAsync(customerResult.Target.Id);
|
||||||
|
// error, throw something
|
||||||
|
}
|
||||||
|
|
||||||
|
user.StripeCustomerId = customerResult.Target.Id;
|
||||||
|
user.StripeSubscriptionId = subResult.Target.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
src/Core/Services/Implementations/StripePaymentService.cs
Normal file
70
src/Core/Services/Implementations/StripePaymentService.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using Stripe;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services
|
||||||
|
{
|
||||||
|
public class StripePaymentService : IPaymentService
|
||||||
|
{
|
||||||
|
private const string PremiumPlanId = "premium-annually";
|
||||||
|
private const string StoragePlanId = "storage-gb-annually";
|
||||||
|
|
||||||
|
public StripePaymentService(
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb)
|
||||||
|
{
|
||||||
|
var customerService = new StripeCustomerService();
|
||||||
|
var customer = await customerService.CreateAsync(new StripeCustomerCreateOptions
|
||||||
|
{
|
||||||
|
Description = user.Name,
|
||||||
|
Email = user.Email,
|
||||||
|
SourceToken = paymentToken
|
||||||
|
});
|
||||||
|
|
||||||
|
var subCreateOptions = new StripeSubscriptionCreateOptions
|
||||||
|
{
|
||||||
|
Items = new List<StripeSubscriptionItemOption>(),
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["userId"] = user.Id.ToString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
||||||
|
{
|
||||||
|
PlanId = PremiumPlanId,
|
||||||
|
Quantity = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if(additionalStorageGb > 0)
|
||||||
|
{
|
||||||
|
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
||||||
|
{
|
||||||
|
PlanId = StoragePlanId,
|
||||||
|
Quantity = additionalStorageGb
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StripeSubscription subscription = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var subscriptionService = new StripeSubscriptionService();
|
||||||
|
subscription = await subscriptionService.CreateAsync(customer.Id, subCreateOptions);
|
||||||
|
}
|
||||||
|
catch(StripeException)
|
||||||
|
{
|
||||||
|
await customerService.DeleteAsync(customer.Id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.StripeCustomerId = customer.Id;
|
||||||
|
user.StripeSubscriptionId = subscription.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -514,55 +514,12 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Already a premium user.");
|
throw new BadRequestException("Already a premium user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var customerService = new StripeCustomerService();
|
IPaymentService paymentService = new StripePaymentService(_globalSettings);
|
||||||
var customer = await customerService.CreateAsync(new StripeCustomerCreateOptions
|
await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb);
|
||||||
{
|
|
||||||
Description = user.Name,
|
|
||||||
Email = user.Email,
|
|
||||||
SourceToken = paymentToken
|
|
||||||
});
|
|
||||||
|
|
||||||
var subCreateOptions = new StripeSubscriptionCreateOptions
|
|
||||||
{
|
|
||||||
Items = new List<StripeSubscriptionItemOption>(),
|
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
["userId"] = user.Id.ToString()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
|
||||||
{
|
|
||||||
PlanId = PremiumPlanId,
|
|
||||||
Quantity = 1
|
|
||||||
});
|
|
||||||
|
|
||||||
if(additionalStorageGb > 0)
|
|
||||||
{
|
|
||||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
|
||||||
{
|
|
||||||
PlanId = StoragePlanId,
|
|
||||||
Quantity = additionalStorageGb
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
StripeSubscription subscription = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var subscriptionService = new StripeSubscriptionService();
|
|
||||||
subscription = await subscriptionService.CreateAsync(customer.Id, subCreateOptions);
|
|
||||||
}
|
|
||||||
catch(StripeException)
|
|
||||||
{
|
|
||||||
await customerService.DeleteAsync(customer.Id);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Premium = true;
|
user.Premium = true;
|
||||||
user.MaxStorageGb = (short)(1 + additionalStorageGb);
|
user.MaxStorageGb = (short)(1 + additionalStorageGb);
|
||||||
user.RevisionDate = DateTime.UtcNow;
|
user.RevisionDate = DateTime.UtcNow;
|
||||||
user.StripeCustomerId = customer.Id;
|
|
||||||
user.StripeSubscriptionId = subscription.Id;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -570,7 +527,7 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
await BillingHelpers.CancelAndRecoverChargesAsync(subscription.Id, customer.Id);
|
await BillingHelpers.CancelAndRecoverChargesAsync(user.StripeSubscriptionId, user.StripeCustomerId);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using Newtonsoft.Json;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -16,6 +17,7 @@ namespace Bit.Core.Utilities
|
|||||||
{
|
{
|
||||||
private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks;
|
private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks;
|
||||||
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
private static readonly Random _random = new Random();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate sequential Guid for Sql Server.
|
/// Generate sequential Guid for Sql Server.
|
||||||
@ -128,34 +130,21 @@ namespace Bit.Core.Utilities
|
|||||||
return globalSettings.U2f.AppId;
|
return globalSettings.U2f.AppId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string RandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
|
||||||
|
bool numeric = true, bool special = false)
|
||||||
|
{
|
||||||
|
return RandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RandomString(int length, string characters)
|
||||||
|
{
|
||||||
|
return new string(Enumerable.Repeat(characters, length).Select(s => s[_random.Next(s.Length)]).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
|
public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
|
||||||
bool numeric = true, bool special = false)
|
bool numeric = true, bool special = false)
|
||||||
{
|
{
|
||||||
var characters = string.Empty;
|
return SecureRandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));
|
||||||
if(alpha)
|
|
||||||
{
|
|
||||||
if(upper)
|
|
||||||
{
|
|
||||||
characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(lower)
|
|
||||||
{
|
|
||||||
characters += "abcdefghijklmnopqrstuvwxyz";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(numeric)
|
|
||||||
{
|
|
||||||
characters += "0123456789";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(special)
|
|
||||||
{
|
|
||||||
characters += "!@#$%^*&";
|
|
||||||
}
|
|
||||||
|
|
||||||
return SecureRandomString(length, characters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ref https://stackoverflow.com/a/8996788/1090359 with modifications
|
// ref https://stackoverflow.com/a/8996788/1090359 with modifications
|
||||||
@ -205,6 +194,35 @@ namespace Bit.Core.Utilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string RandomStringCharacters(bool alpha, bool upper, bool lower, bool numeric, bool special)
|
||||||
|
{
|
||||||
|
var characters = string.Empty;
|
||||||
|
if(alpha)
|
||||||
|
{
|
||||||
|
if(upper)
|
||||||
|
{
|
||||||
|
characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lower)
|
||||||
|
{
|
||||||
|
characters += "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(numeric)
|
||||||
|
{
|
||||||
|
characters += "0123456789";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(special)
|
||||||
|
{
|
||||||
|
characters += "!@#$%^*&";
|
||||||
|
}
|
||||||
|
|
||||||
|
return characters;
|
||||||
|
}
|
||||||
|
|
||||||
// ref: https://stackoverflow.com/a/11124118/1090359
|
// ref: https://stackoverflow.com/a/11124118/1090359
|
||||||
// Returns the human-readable file size for an arbitrary 64-bit file size .
|
// Returns the human-readable file size for an arbitrary 64-bit file size .
|
||||||
// The format is "0.## XB", ex: "4.2 KB" or "1.43 GB"
|
// The format is "0.## XB", ex: "4.2 KB" or "1.43 GB"
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
"baseVaultUri": "https://vault.bitwarden.com/#",
|
"baseVaultUri": "https://vault.bitwarden.com/#",
|
||||||
"u2f": {
|
"u2f": {
|
||||||
"appId": "https://vault.bitwarden.com/app-id.json"
|
"appId": "https://vault.bitwarden.com/app-id.json"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,12 @@
|
|||||||
},
|
},
|
||||||
"u2f": {
|
"u2f": {
|
||||||
"appId": "https://localhost:4001/app-id.json"
|
"appId": "https://localhost:4001/app-id.json"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": false,
|
||||||
|
"merchantId": "SECRET",
|
||||||
|
"publicKey": "SECRET",
|
||||||
|
"privateKey": "SECRET"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user