diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index f02380af3f..c7948a9a26 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -12,9 +12,6 @@ { public virtual bool Production { get; set; } public virtual string BusinessId { get; set; } - public virtual string ClientId { get; set; } - public virtual string ClientSecret { get; set; } - public virtual string WebhookId { get; set; } public virtual string WebhookKey { get; set; } } } diff --git a/src/Billing/Controllers/PayPalController.cs b/src/Billing/Controllers/PayPalController.cs index 78008b2b60..c7151443ec 100644 --- a/src/Billing/Controllers/PayPalController.cs +++ b/src/Billing/Controllers/PayPalController.cs @@ -6,7 +6,6 @@ using Bit.Core.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using System.Data.SqlClient; using System.IO; using System.Text; @@ -18,7 +17,6 @@ namespace Bit.Billing.Controllers public class PayPalController : Controller { private readonly BillingSettings _billingSettings; - private readonly PayPalClient _paypalClient; private readonly PayPalIpnClient _paypalIpnClient; private readonly ITransactionRepository _transactionRepository; private readonly IOrganizationRepository _organizationRepository; @@ -29,7 +27,6 @@ namespace Bit.Billing.Controllers public PayPalController( IOptions billingSettings, - PayPalClient paypalClient, PayPalIpnClient paypalIpnClient, ITransactionRepository transactionRepository, IOrganizationRepository organizationRepository, @@ -39,7 +36,6 @@ namespace Bit.Billing.Controllers ILogger logger) { _billingSettings = billingSettings?.Value; - _paypalClient = paypalClient; _paypalIpnClient = paypalIpnClient; _transactionRepository = transactionRepository; _organizationRepository = organizationRepository; @@ -49,116 +45,6 @@ namespace Bit.Billing.Controllers _logger = logger; } - [HttpPost("webhook")] - public async Task PostWebhook([FromQuery] string key) - { - if(key != _billingSettings.PayPal.WebhookKey) - { - return new BadRequestResult(); - } - - if(HttpContext?.Request == null) - { - return new BadRequestResult(); - } - - string body = null; - using(var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8)) - { - body = await reader.ReadToEndAsync(); - } - - if(string.IsNullOrWhiteSpace(body)) - { - return new BadRequestResult(); - } - - var verified = await _paypalClient.VerifyWebhookAsync(body, HttpContext.Request.Headers, - _billingSettings.PayPal.WebhookId); - if(!verified) - { - return new BadRequestResult(); - } - - if(body.Contains("\"PAYMENT.SALE.COMPLETED\"")) - { - var ev = JsonConvert.DeserializeObject>(body); - var sale = ev.Resource; - var saleTransaction = await _transactionRepository.GetByGatewayIdAsync( - GatewayType.PayPal, sale.Id); - if(saleTransaction == null) - { - var ids = sale.GetIdsFromCustom(); - if(ids.Item1.HasValue || ids.Item2.HasValue) - { - try - { - await _transactionRepository.CreateAsync(new Core.Models.Table.Transaction - { - Amount = sale.Amount.TotalAmount, - CreationDate = sale.CreateTime, - OrganizationId = ids.Item1, - UserId = ids.Item2, - Type = TransactionType.Charge, - Gateway = GatewayType.PayPal, - GatewayId = sale.Id, - PaymentMethodType = PaymentMethodType.PayPal, - Details = sale.Id - }); - } - // Catch foreign key violations because user/org could have been deleted. - catch(SqlException e) when(e.Number == 547) { } - } - } - } - else if(body.Contains("\"PAYMENT.SALE.REFUNDED\"")) - { - var ev = JsonConvert.DeserializeObject>(body); - var refund = ev.Resource; - var refundTransaction = await _transactionRepository.GetByGatewayIdAsync( - GatewayType.PayPal, refund.Id); - if(refundTransaction == null) - { - var saleTransaction = await _transactionRepository.GetByGatewayIdAsync( - GatewayType.PayPal, refund.SaleId); - if(saleTransaction == null) - { - return new BadRequestResult(); - } - - if(!saleTransaction.Refunded.GetValueOrDefault() && - saleTransaction.RefundedAmount.GetValueOrDefault() < refund.TotalRefundedAmount.ValueAmount) - { - saleTransaction.RefundedAmount = refund.TotalRefundedAmount.ValueAmount; - if(saleTransaction.RefundedAmount == saleTransaction.Amount) - { - saleTransaction.Refunded = true; - } - await _transactionRepository.ReplaceAsync(saleTransaction); - - var ids = refund.GetIdsFromCustom(); - if(ids.Item1.HasValue || ids.Item2.HasValue) - { - await _transactionRepository.CreateAsync(new Core.Models.Table.Transaction - { - Amount = refund.Amount.TotalAmount, - CreationDate = refund.CreateTime, - OrganizationId = ids.Item1, - UserId = ids.Item2, - Type = TransactionType.Refund, - Gateway = GatewayType.PayPal, - GatewayId = refund.Id, - PaymentMethodType = PaymentMethodType.PayPal, - Details = refund.Id - }); - } - } - } - } - - return new OkResult(); - } - [HttpPost("ipn")] public async Task PostIpn() { @@ -226,13 +112,6 @@ namespace Bit.Billing.Controllers return new OkResult(); } - var isAccountCredit = ipnTransaction.IsAccountCredit(); - if(!isAccountCredit) - { - // Only processing credits via IPN for now - return new OkResult(); - } - if(ipnTransaction.PaymentStatus == "Completed") { var transaction = await _transactionRepository.GetByGatewayIdAsync( @@ -243,6 +122,7 @@ namespace Bit.Billing.Controllers return new OkResult(); } + var isAccountCredit = ipnTransaction.IsAccountCredit(); try { var tx = new Transaction @@ -259,7 +139,7 @@ namespace Bit.Billing.Controllers }; await _transactionRepository.CreateAsync(tx); - if(ipnTransaction.IsAccountCredit()) + if(isAccountCredit) { string billingEmail = null; if(tx.OrganizationId.HasValue) diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index bc288235c6..69a3d3ec8e 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -39,8 +39,7 @@ namespace Bit.Billing // Repositories services.AddSqlServerRepositories(globalSettings); - // PayPal Clients - services.AddSingleton(); + // PayPal Client services.AddSingleton(); // BitPay Client diff --git a/src/Billing/Utilities/PayPalClient.cs b/src/Billing/Utilities/PayPalClient.cs deleted file mode 100644 index 4a193a45ce..0000000000 --- a/src/Billing/Utilities/PayPalClient.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; - -namespace Bit.Billing.Utilities -{ - public class PayPalClient - { - private readonly HttpClient _httpClient = new HttpClient(); - private readonly string _baseApiUrl; - private readonly string _clientId; - private readonly string _clientSecret; - - private AuthResponse _authResponse; - - public PayPalClient(IOptions billingSettings) - { - var bSettings = billingSettings?.Value; - _baseApiUrl = _baseApiUrl = !bSettings.PayPal.Production ? "https://api.sandbox.paypal.com/{0}" : - "https://api.paypal.com/{0}"; - _clientId = bSettings.PayPal.ClientId; - _clientSecret = bSettings.PayPal.ClientSecret; - } - - public async Task VerifyWebhookAsync(string webhookJson, IHeaderDictionary headers, string webhookId) - { - if(webhookJson == null) - { - throw new ArgumentException("No webhook json."); - } - - if(headers == null) - { - throw new ArgumentException("No headers."); - } - - if(!headers.ContainsKey("PAYPAL-TRANSMISSION-ID")) - { - return false; - } - - await AuthIfNeededAsync(); - - var req = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Format(_baseApiUrl, "v1/notifications/verify-webhook-signature")) - }; - req.Headers.Authorization = new AuthenticationHeaderValue( - _authResponse.TokenType, _authResponse.AccessToken); - req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - var verifyRequest = new VerifyWebookRequest - { - AuthAlgo = headers["PAYPAL-AUTH-ALGO"], - CertUrl = headers["PAYPAL-CERT-URL"], - TransmissionId = headers["PAYPAL-TRANSMISSION-ID"], - TransmissionTime = headers["PAYPAL-TRANSMISSION-TIME"], - TransmissionSig = headers["PAYPAL-TRANSMISSION-SIG"], - WebhookId = webhookId - }; - var verifyRequestJson = JsonConvert.SerializeObject(verifyRequest); - verifyRequestJson = verifyRequestJson.Replace("\"__WEBHOOK_BODY__\"", webhookJson); - req.Content = new StringContent(verifyRequestJson, Encoding.UTF8, "application/json"); - - var response = await _httpClient.SendAsync(req); - if(!response.IsSuccessStatusCode) - { - throw new Exception("Failed to verify webhook"); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var verifyResponse = JsonConvert.DeserializeObject(responseContent); - return verifyResponse.Verified; - } - - private async Task AuthIfNeededAsync() - { - if(_authResponse?.Expired ?? true) - { - var req = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Format(_baseApiUrl, "v1/oauth2/token")) - }; - var authVal = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}")); - req.Headers.Authorization = new AuthenticationHeaderValue("Basic", authVal); - req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - req.Content = new FormUrlEncodedContent(new[] - { - new KeyValuePair("grant_type", "client_credentials") - }); - - var response = await _httpClient.SendAsync(req); - if(!response.IsSuccessStatusCode) - { - throw new Exception("Failed to auth with PayPal"); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - _authResponse = JsonConvert.DeserializeObject(responseContent); - return true; - } - return false; - } - - public class VerifyWebookRequest - { - [JsonProperty("auth_algo")] - public string AuthAlgo { get; set; } - [JsonProperty("cert_url")] - public string CertUrl { get; set; } - [JsonProperty("transmission_id")] - public string TransmissionId { get; set; } - [JsonProperty("transmission_sig")] - public string TransmissionSig { get; set; } - [JsonProperty("transmission_time")] - public string TransmissionTime { get; set; } - [JsonProperty("webhook_event")] - public string WebhookEvent { get; set; } = "__WEBHOOK_BODY__"; - [JsonProperty("webhook_id")] - public string WebhookId { get; set; } - } - - public class VerifyWebookResponse - { - [JsonProperty("verification_status")] - public string VerificationStatus { get; set; } - public bool Verified => VerificationStatus == "SUCCESS"; - } - - public class AuthResponse - { - private DateTime _created; - - public AuthResponse() - { - _created = DateTime.UtcNow; - } - - [JsonProperty("scope")] - public string Scope { get; set; } - [JsonProperty("nonce")] - public string Nonce { get; set; } - [JsonProperty("access_token")] - public string AccessToken { get; set; } - [JsonProperty("token_type")] - public string TokenType { get; set; } - [JsonProperty("app_id")] - public string AppId { get; set; } - [JsonProperty("expires_in")] - public long ExpiresIn { get; set; } - public bool Expired => DateTime.UtcNow > _created.AddSeconds(ExpiresIn - 30); - } - - public class Event - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("event_type")] - public string EventType { get; set; } - [JsonProperty("resource_type")] - public string ResourceType { get; set; } - [JsonProperty("create_time")] - public DateTime CreateTime { get; set; } - public T Resource { get; set; } - } - - public class Refund : Sale - { - [JsonProperty("total_refunded_amount")] - public ValueInfo TotalRefundedAmount { get; set; } - [JsonProperty("sale_id")] - public string SaleId { get; set; } - } - - public class Sale - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("state")] - public string State { get; set; } - [JsonProperty("amount")] - public AmountInfo Amount { get; set; } - [JsonProperty("parent_payment")] - public string ParentPayment { get; set; } - [JsonProperty("custom")] - public string Custom { get; set; } - [JsonProperty("create_time")] - public DateTime CreateTime { get; set; } - [JsonProperty("update_time")] - public DateTime UpdateTime { get; set; } - - public Tuple GetIdsFromCustom() - { - Guid? orgId = null; - Guid? userId = null; - - if(!string.IsNullOrWhiteSpace(Custom) && Custom.Contains(":")) - { - var parts = Custom.Split(':'); - if(parts.Length > 1 && Guid.TryParse(parts[1], out var id)) - { - if(parts[0] == "user_id") - { - userId = id; - } - else if(parts[0] == "organization_id") - { - orgId = id; - } - } - } - - return new Tuple(orgId, userId); - } - } - - public class AmountInfo - { - [JsonProperty("total")] - public string Total { get; set; } - public decimal TotalAmount => Convert.ToDecimal(Total); - } - - public class ValueInfo - { - [JsonProperty("value")] - public string Value { get; set; } - public decimal ValueAmount => Convert.ToDecimal(Value); - } - } -} diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index 40d458262e..19da86dccc 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -66,9 +66,6 @@ "payPal": { "production": false, "businessId": "AD3LAUZSNVPJY", - "clientId": "SECRET", - "clientSecret": "SECRET", - "webhookId": "SECRET", "webhookKey": "SECRET" } }