diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index d1367e689f..274774bd4c 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -7,6 +7,8 @@ public virtual string StripeWebhookSecret { get; set; } public virtual string BitPayWebhookKey { get; set; } public virtual string AppleWebhookKey { get; set; } + public virtual string FreshdeskWebhookKey { get; set; } + public virtual string FreshdeskApiKey { get; set; } public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings(); public class PayPalSettings diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs new file mode 100644 index 0000000000..5b520aa9ae --- /dev/null +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -0,0 +1,150 @@ +using Bit.Core; +using Bit.Core.Repositories; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Billing.Controllers +{ + [Route("freshdesk")] + public class FreshdeskController : Controller + { + private readonly BillingSettings _billingSettings; + private readonly IUserRepository _userRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ILogger _logger; + private readonly HttpClient _httpClient = new HttpClient(); + + public FreshdeskController( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOptions billingSettings, + ILogger logger) + { + _billingSettings = billingSettings?.Value; + _userRepository = userRepository; + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _logger = logger; + } + + [HttpPost("webhook")] + public async Task PostWebhook() + { + if(HttpContext?.Request?.Query == null) + { + return new BadRequestResult(); + } + + var key = HttpContext.Request.Query.ContainsKey("key") ? + HttpContext.Request.Query["key"].ToString() : null; + if(key != _billingSettings.FreshdeskWebhookKey) + { + 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(); + } + + try + { + dynamic data = JsonConvert.DeserializeObject(body); + string ticketId = data.ticket_id; + string ticketContactEmail = data.ticket_contact_email; + if(string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail)) + { + return new BadRequestResult(); + } + + var updateBody = new Dictionary(); + var user = await _userRepository.GetByEmailAsync(ticketContactEmail); + if(user != null) + { + var tags = new List(); + if(user.Premium) + { + tags.Add("Premium"); + } + var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); + if(orgs.Any()) + { + foreach(var org in orgs) + { + tags.Add("Org:" + GetAttribute(org.PlanType).Name); + } + } + var hasPaidOrg = orgs.Any(o => o.PlanType != Core.Enums.PlanType.Free); + if(user.Premium || hasPaidOrg) + { + updateBody.Add("priority", 3); + } + if(tags.Any()) + { + updateBody.Add("tags", tags); + } + + var request = new HttpRequestMessage(HttpMethod.Put, + string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", ticketId)); + request.Content = new StringContent(JsonConvert.SerializeObject(updateBody), + Encoding.UTF8, "application/json"); + await CallFreshdeskApiAsync(request); + } + + return new OkResult(); + } + catch(Exception e) + { + _logger.LogError(e, "Error processing freshdesk webhook."); + return new BadRequestResult(); + } + } + + private async Task CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0) + { + try + { + var authKey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_billingSettings.FreshdeskApiKey}:X")); + request.Headers.Add("Authorization", authKey); + var response = await _httpClient.SendAsync(request); + if(response.StatusCode != System.Net.HttpStatusCode.TooManyRequests || retriedCount > 3) + { + return response; + } + } + catch + { + if(retriedCount > 3) + { + throw; + } + } + await Task.Delay(60000 * (retriedCount + 1)); + return await CallFreshdeskApiAsync(request, retriedCount++); + } + + private TAttribute GetAttribute(Enum enumValue) where TAttribute : Attribute + { + return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute(); + } + } +}