Browse Source

Added billing/subscriptions

feature/billing
Teknikode 8 months ago
parent
commit
543df28eb7
  1. 2
      BillingCore/BillingFactory.cs
  2. 18
      BillingCore/BillingService.cs
  3. 17
      BillingCore/Models/CheckoutResult.cs
  4. 17
      BillingCore/Models/CheckoutSession.cs
  5. 20
      BillingCore/Models/Customer.cs
  6. 15
      BillingCore/Models/Event.cs
  7. 16
      BillingCore/Models/EventType.cs
  8. 15
      BillingCore/Models/PaymentStatus.cs
  9. 1
      BillingCore/Models/Price.cs
  10. 1
      BillingCore/Models/Subscription.cs
  11. 320
      BillingCore/StripeService.cs
  12. 44
      BillingService/Program.cs
  13. 2
      Configuration/BillingConfig.cs
  14. 27
      Configuration/UploadConfig.cs
  15. 11
      ServiceWorker/Program.cs
  16. 47
      ServiceWorker/TeknikMigration.cs
  17. 165
      Teknik/App_Data/endpointMappings.json
  18. 104
      Teknik/Areas/API/V1/Controllers/BillingAPIv1Controller.cs
  19. 15
      Teknik/Areas/API/V1/Controllers/UploadAPIv1Controller.cs
  20. 121
      Teknik/Areas/About/Controllers/AboutController.cs
  21. 7
      Teknik/Areas/About/ViewModels/AboutViewModel.cs
  22. 100
      Teknik/Areas/About/Views/About/Index.cshtml
  23. 217
      Teknik/Areas/Billing/Controllers/BillingController.cs
  24. 19
      Teknik/Areas/Billing/Models/Customer.cs
  25. 13
      Teknik/Areas/Billing/ViewModels/CancelSubscriptionViewModel.cs
  26. 17
      Teknik/Areas/Billing/ViewModels/PriceViewModel.cs
  27. 15
      Teknik/Areas/Billing/ViewModels/SubscribeViewModel.cs
  28. 20
      Teknik/Areas/Billing/ViewModels/SubscriptionSuccessViewModel.cs
  29. 6
      Teknik/Areas/Billing/ViewModels/SubscriptionViewModel.cs
  30. 21
      Teknik/Areas/Billing/Views/Billing/CancelSubscription.cshtml
  31. 45
      Teknik/Areas/Billing/Views/Billing/Subscribe.cshtml
  32. 21
      Teknik/Areas/Billing/Views/Billing/SubscriptionSuccess.cshtml
  33. 13
      Teknik/Areas/Billing/Views/Billing/ViewSubscription.cshtml
  34. 41
      Teknik/Areas/Billing/Views/Billing/ViewSubscriptions.cshtml
  35. 2
      Teknik/Areas/FAQ/Views/FAQ/Index.cshtml
  36. 10
      Teknik/Areas/Help/Controllers/HelpController.cs
  37. 12
      Teknik/Areas/Home/Views/Home/Index.cshtml
  38. 12
      Teknik/Areas/Paste/Controllers/PasteController.cs
  39. 22
      Teknik/Areas/Paste/PasteHelper.cs
  40. 42
      Teknik/Areas/Upload/Controllers/UploadController.cs
  41. 46
      Teknik/Areas/User/Controllers/UserController.cs
  42. 6
      Teknik/Areas/User/Models/UploadSettings.cs
  43. 3
      Teknik/Areas/User/Models/User.cs
  44. 18
      Teknik/Areas/User/ViewModels/BillingSettingsViewModel.cs
  45. 23
      Teknik/Areas/User/ViewModels/SubscriptionViewModel.cs
  46. 32
      Teknik/Areas/User/Views/User/Settings/BillingSettings.cshtml
  47. 1
      Teknik/Areas/User/Views/User/Settings/Settings.cshtml
  48. 1057
      Teknik/Data/Migrations/20211009192142_AddBillingCustomers.Designer.cs
  49. 40
      Teknik/Data/Migrations/20211009192142_AddBillingCustomers.cs
  50. 1061
      Teknik/Data/Migrations/20211113052031_RenameUploadSettings.Designer.cs
  51. 23
      Teknik/Data/Migrations/20211113052031_RenameUploadSettings.cs
  52. 547
      Teknik/Data/Migrations/TeknikEntitiesModelSnapshot.cs
  53. 9
      Teknik/Data/TeknikEntities.cs
  54. BIN
      Teknik/Images/logo-blue-1920.png
  55. BIN
      Teknik/Images/logo-blue-2400.png
  56. 49
      Teknik/Scripts/Billing/Subscriptions.js
  57. 26
      Utilities/Cryptography/Crypto.cs

2
BillingCore/BillingFactory.cs

@ -9,7 +9,7 @@ namespace Teknik.BillingCore @@ -9,7 +9,7 @@ namespace Teknik.BillingCore
{
public static class BillingFactory
{
public static BillingService GetStorageService(BillingConfig config)
public static BillingService GetBillingService(BillingConfig config)
{
switch (config.Type)
{

18
BillingCore/BillingService.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -18,7 +19,7 @@ namespace Teknik.BillingCore @@ -18,7 +19,7 @@ namespace Teknik.BillingCore
}
public abstract object GetCustomer(string id);
public abstract bool CreateCustomer(string email);
public abstract string CreateCustomer(string username, string email);
public abstract List<Product> GetProductList();
public abstract Product GetProduct(string productId);
@ -28,10 +29,15 @@ namespace Teknik.BillingCore @@ -28,10 +29,15 @@ namespace Teknik.BillingCore
public abstract List<Subscription> GetSubscriptionList(string customerId);
public abstract Subscription GetSubscription(string subscriptionId);
public abstract Tuple<bool, string, string> CreateSubscription(string customerId, string priceId);
public abstract bool EditSubscription(Subscription subscription);
public abstract bool RemoveSubscription(string subscriptionId);
public abstract Subscription CreateSubscription(string customerId, string priceId);
public abstract Subscription EditSubscriptionPrice(string subscriptionId, string priceId);
public abstract bool CancelSubscription(string subscriptionId);
public abstract void SyncSubscriptions();
public abstract CheckoutSession CreateCheckoutSession(string customerId, string priceId, string successUrl, string cancelUrl);
public abstract CheckoutSession GetCheckoutSession(string sessionId);
public abstract Task<Event> ParseEvent(HttpRequest request);
public abstract CheckoutSession ProcessCheckoutCompletedEvent(Event e);
public abstract Customer ProcessCustomerEvent(Event e);
}
}

17
BillingCore/Models/CheckoutResult.cs

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Teknik.BillingCore.Models
{
public class CheckoutResult
{
public string CustomerId { get; set; }
public string SubscriptionId { get; set; }
public PaymentStatus PaymentStatus { get; set; }
}
}

17
BillingCore/Models/CheckoutSession.cs

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Teknik.BillingCore.Models
{
public class CheckoutSession
{
public string PaymentIntentId { get; set; }
public string CustomerId { get; set; }
public string SubscriptionId { get; set; }
public PaymentStatus PaymentStatus { get; set; }
public string Url { get; set; }
}
}

20
BillingCore/Models/Customer.cs

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Teknik.BillingCore.Models
{
public class Customer
{
public string CustomerId { get; set; }
public List<Subscription> Subscriptions { get; set; }
public Customer()
{
Subscriptions = new List<Subscription>();
}
}
}

15
BillingCore/Models/Event.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Teknik.BillingCore.Models
{
public class Event
{
public EventType EventType { get; set; }
public object Data { get; set; }
}
}

16
BillingCore/Models/EventType.cs

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Teknik.BillingCore.Models
{
public enum EventType
{
Unknown,
CheckoutComplete,
SubscriptionDeleted,
SubscriptionUpdated
}
}

15
BillingCore/Models/PaymentStatus.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Teknik.BillingCore.Models
{
public enum PaymentStatus
{
Paid,
Unpaid,
NoPaymentRequired
}
}

1
BillingCore/Models/Price.cs

@ -16,6 +16,7 @@ namespace Teknik.BillingCore.Models @@ -16,6 +16,7 @@ namespace Teknik.BillingCore.Models
public decimal? Amount { get; set; }
public string Currency { get; set; }
public long Storage { get; set; }
public long FileSize { get; set; }
public Interval Interval { get; set; }
}
}

1
BillingCore/Models/Subscription.cs

@ -12,5 +12,6 @@ namespace Teknik.BillingCore.Models @@ -12,5 +12,6 @@ namespace Teknik.BillingCore.Models
public string CustomerId { get; set; }
public SubscriptionStatus Status { get; set; }
public List<Price> Prices { get; set; }
public string ClientSecret { get; set; }
}
}

320
BillingCore/StripeService.cs

@ -1,9 +1,12 @@ @@ -1,9 +1,12 @@
using Stripe;
using Microsoft.AspNetCore.Http;
using Stripe;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Teknik.BillingCore.Models;
using Teknik.Configuration;
@ -16,21 +19,33 @@ namespace Teknik.BillingCore @@ -16,21 +19,33 @@ namespace Teknik.BillingCore
StripeConfiguration.ApiKey = config.StripeSecretApiKey;
}
public override object GetCustomer(string id)
public override string GetCustomer(string email)
{
var service = new CustomerService();
return service.Get(id);
if (!string.IsNullOrEmpty(email))
{
var service = new CustomerService();
var foundCustomer = service.Get(email);
if (foundCustomer != null)
return foundCustomer.Id;
}
return null;
}
public override bool CreateCustomer(string email)
public override string CreateCustomer(string username, string email)
{
if (string.IsNullOrEmpty(username))
throw new ArgumentNullException("username");
var options = new CustomerCreateOptions
{
Name = username,
Email = email,
Description = $"Customer for account {username}"
};
var service = new CustomerService();
var customer = service.Create(options);
return customer != null;
return customer.Id;
}
public override List<Models.Product> GetProductList()
@ -52,10 +67,13 @@ namespace Teknik.BillingCore @@ -52,10 +67,13 @@ namespace Teknik.BillingCore
public override Models.Product GetProduct(string productId)
{
var productService = new ProductService();
Stripe.Product product = productService.Get(productId);
if (product != null)
return ConvertProduct(product);
if (!string.IsNullOrEmpty(productId))
{
var productService = new ProductService();
Stripe.Product product = productService.Get(productId);
if (product != null)
return ConvertProduct(product);
}
return null;
}
@ -63,31 +81,39 @@ namespace Teknik.BillingCore @@ -63,31 +81,39 @@ namespace Teknik.BillingCore
public override List<Models.Price> GetPriceList(string productId)
{
var foundPrices = new List<Models.Price>();
var options = new PriceListOptions
if (!string.IsNullOrEmpty(productId))
{
Active = true,
Product = productId
};
var options = new PriceListOptions
{
Active = true,
Product = productId
};
options.AddExpand("data.product");
var priceService = new PriceService();
var priceList = priceService.List(options);
if (priceList != null)
{
foreach (var price in priceList)
var priceService = new PriceService();
var priceList = priceService.List(options);
if (priceList != null)
{
foundPrices.Add(ConvertPrice(price));
foreach (var price in priceList)
{
foundPrices.Add(ConvertPrice(price));
}
}
}
return foundPrices;
}
public override Models.Price GetPrice(string priceId)
{
var priceService = new PriceService();
var price = priceService.Get(priceId);
if (price != null)
return ConvertPrice(price);
if (!string.IsNullOrEmpty(priceId))
{
var options = new PriceGetOptions();
var priceService = new PriceService();
var price = priceService.Get(priceId, options);
if (price != null)
return ConvertPrice(price);
}
return null;
}
@ -118,62 +144,176 @@ namespace Teknik.BillingCore @@ -118,62 +144,176 @@ namespace Teknik.BillingCore
public override Models.Subscription GetSubscription(string subscriptionId)
{
var subService = new SubscriptionService();
var sub = subService.Get(subscriptionId);
if (sub != null)
return ConvertSubscription(sub);
if (!string.IsNullOrEmpty(subscriptionId))
{
var subService = new SubscriptionService();
var sub = subService.Get(subscriptionId);
if (sub != null)
return ConvertSubscription(sub);
}
return null;
}
public override Tuple<bool, string, string> CreateSubscription(string customerId, string priceId)
public override Models.Subscription CreateSubscription(string customerId, string priceId)
{
// Create the subscription. Note we're expanding the Subscription's
// latest invoice and that invoice's payment_intent
// so we can pass it to the front end to confirm the payment
var subscriptionOptions = new SubscriptionCreateOptions
if (!string.IsNullOrEmpty(customerId) &&
!string.IsNullOrEmpty(priceId))
{
Customer = customerId,
Items = new List<SubscriptionItemOptions>
// Create the subscription. Note we're expanding the Subscription's
// latest invoice and that invoice's payment_intent
// so we can pass it to the front end to confirm the payment
var subscriptionOptions = new SubscriptionCreateOptions
{
new SubscriptionItemOptions
Customer = customerId,
Items = new List<SubscriptionItemOptions>
{
Price = priceId,
new SubscriptionItemOptions
{
Price = priceId,
},
},
PaymentBehavior = "default_incomplete",
CancelAtPeriodEnd = false
};
subscriptionOptions.AddExpand("latest_invoice.payment_intent");
var subscriptionService = new SubscriptionService();
var subscription = subscriptionService.Create(subscriptionOptions);
return ConvertSubscription(subscription);
}
return null;
}
public override Models.Subscription EditSubscriptionPrice(string subscriptionId, string priceId)
{
if (!string.IsNullOrEmpty(subscriptionId))
{
var subscriptionService = new SubscriptionService();
var subscription = subscriptionService.Get(subscriptionId);
if (subscription != null)
{
var subscriptionOptions = new SubscriptionUpdateOptions()
{
Items = new List<SubscriptionItemOptions>
{
new SubscriptionItemOptions
{
Id = subscription.Items.Data[0].Id,
Price = priceId,
},
},
CancelAtPeriodEnd = false,
ProrationBehavior = "create_prorations"
};
subscriptionOptions.AddExpand("latest_invoice.payment_intent");
var result = subscriptionService.Update(subscriptionId, subscriptionOptions);
if (result != null)
return ConvertSubscription(result);
}
}
return null;
}
public override bool CancelSubscription(string subscriptionId)
{
if (!string.IsNullOrEmpty(subscriptionId))
{
var cancelOptions = new SubscriptionCancelOptions()
{
InvoiceNow = true
};
var subscriptionService = new SubscriptionService();
var subscription = subscriptionService.Cancel(subscriptionId, cancelOptions);
return subscription.Status == "canceled";
}
return false;
}
public override CheckoutSession CreateCheckoutSession(string customerId, string priceId, string successUrl, string cancelUrl)
{
// Modify Success URL to include session ID variable
var uriBuilder = new UriBuilder(successUrl);
var paramValues = HttpUtility.ParseQueryString(uriBuilder.Query);
paramValues.Add("session_id", "{CHECKOUT_SESSION_ID}");
uriBuilder.Query = paramValues.ToString();
successUrl = uriBuilder.Uri.ToString();
var checkoutService = new Stripe.Checkout.SessionService();
var sessionOptions = new Stripe.Checkout.SessionCreateOptions()
{
LineItems = new List<Stripe.Checkout.SessionLineItemOptions>()
{
new Stripe.Checkout.SessionLineItemOptions()
{
Price = priceId,
Quantity = 1
}
},
PaymentBehavior = "default_incomplete",
PaymentMethodTypes = new List<string>()
{
"card"
},
Mode = "subscription",
SuccessUrl = successUrl,
CancelUrl = cancelUrl,
Customer = customerId
};
subscriptionOptions.AddExpand("latest_invoice.payment_intent");
var subscriptionService = new SubscriptionService();
sessionOptions.AddExpand("customer");
var session = checkoutService.Create(sessionOptions);
return ConvertCheckoutSession(session);
}
public override async Task<Models.Event> ParseEvent(HttpRequest request)
{
var json = await new StreamReader(request.Body).ReadToEndAsync();
try
{
Stripe.Subscription subscription = subscriptionService.Create(subscriptionOptions);
var stripeEvent = EventUtility.ConstructEvent(
json,
request.Headers["Stripe-Signature"],
Config.StripeWebhookSecret
);
return new Tuple<bool, string, string>(true, subscription.Id, subscription.LatestInvoice.PaymentIntent.ClientSecret);
return ConvertEvent(stripeEvent);
}
catch (StripeException e)
catch (StripeException)
{
return new Tuple<bool, string, string>(false, $"Failed to create subscription. {e}", null);
}
return null;
}
public override bool EditSubscription(Models.Subscription subscription)
public override CheckoutSession ProcessCheckoutCompletedEvent(Models.Event ev)
{
throw new NotImplementedException();
// Handle the checkout.session.completed event
var session = ev.Data as Stripe.Checkout.Session;
return ConvertCheckoutSession(session);
}
public override bool RemoveSubscription(string subscriptionId)
public override Models.Customer ProcessCustomerEvent(Models.Event ev)
{
throw new NotImplementedException();
// Handle the checkout.session.completed event
var customer = ev.Data as Stripe.Customer;
return ConvertCustomer(customer);
}
public override void SyncSubscriptions()
public override CheckoutSession GetCheckoutSession(string sessionId)
{
throw new NotImplementedException();
var checkoutService = new Stripe.Checkout.SessionService();
var sessionOptions = new Stripe.Checkout.SessionGetOptions();
sessionOptions.AddExpand("customer");
var session = checkoutService.Get(sessionId, sessionOptions);
return ConvertCheckoutSession(session);
}
private Models.Product ConvertProduct(Stripe.Product product)
{
if (product == null)
return null;
return new Models.Product()
{
ProductId = product.Id,
@ -185,6 +325,8 @@ namespace Teknik.BillingCore @@ -185,6 +325,8 @@ namespace Teknik.BillingCore
private Models.Price ConvertPrice(Stripe.Price price)
{
if (price == null)
return null;
var interval = Interval.Once;
if (price.Type == "recurring")
{
@ -216,11 +358,15 @@ namespace Teknik.BillingCore @@ -216,11 +358,15 @@ namespace Teknik.BillingCore
convPrice.Amount = price.UnitAmountDecimal / 100;
if (price.Metadata.ContainsKey("storage"))
convPrice.Storage = long.Parse(price.Metadata["storage"]);
if (price.Metadata.ContainsKey("fileSize"))
convPrice.FileSize = long.Parse(price.Metadata["fileSize"]);
return convPrice;
}
private Models.Subscription ConvertSubscription(Stripe.Subscription subscription)
{
if (subscription == null)
return null;
var status = SubscriptionStatus.Incomplete;
switch (subscription.Status)
{
@ -259,7 +405,77 @@ namespace Teknik.BillingCore @@ -259,7 +405,77 @@ namespace Teknik.BillingCore
Id = subscription.Id,
CustomerId = subscription.CustomerId,
Status = status,
Prices = prices
Prices = prices,
ClientSecret = subscription.LatestInvoice?.PaymentIntent?.ClientSecret
};
}
private CheckoutSession ConvertCheckoutSession(Stripe.Checkout.Session session)
{
if (session == null)
return null;
var paymentStatus = PaymentStatus.Unpaid;
switch (session.PaymentStatus)
{
case "paid":
paymentStatus = PaymentStatus.Paid;
break;
case "unpaid":
paymentStatus = PaymentStatus.Unpaid;
break;
case "no_payment_required":
paymentStatus = PaymentStatus.NoPaymentRequired;
break;
}
return new CheckoutSession()
{
PaymentIntentId = session.PaymentIntentId,
CustomerId = session.Customer.Id,
SubscriptionId = session.SubscriptionId,
PaymentStatus = paymentStatus,
Url = session.Url
};
}
private Models.Customer ConvertCustomer(Stripe.Customer customer)
{
var returnCust = new Models.Customer()
{
CustomerId = customer.Id
};
if (customer.Subscriptions.Any())
returnCust.Subscriptions = customer.Subscriptions.Select(s => ConvertSubscription(s)).ToList();
return returnCust;
}
private Models.Event ConvertEvent(Stripe.Event ev)
{
if (ev == null)
return null;
var eventType = EventType.Unknown;
switch (ev.Type)
{
case Events.CheckoutSessionCompleted:
eventType = EventType.CheckoutComplete;
break;
case Events.CustomerSubscriptionDeleted:
eventType = EventType.SubscriptionDeleted;
break;
case Events.CustomerSubscriptionUpdated:
eventType = EventType.SubscriptionUpdated;
break;
}
return new Models.Event()
{
EventType = eventType,
Data = ev.Data.Object
};
}
}

44
BillingService/Program.cs

@ -1,9 +1,14 @@ @@ -1,9 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using CommandLine;
using Microsoft.EntityFrameworkCore;
using Teknik.Areas.Users.Models;
using Teknik.Areas.Users.Utility;
using Teknik.BillingCore;
using Teknik.BillingCore.Models;
using Teknik.Configuration;
using Teknik.Data;
using Teknik.Utilities;
@ -43,6 +48,7 @@ namespace Teknik.BillingService @@ -43,6 +48,7 @@ namespace Teknik.BillingService
if (options.SyncSubscriptions)
{
// Sync subscription information
SyncSubscriptions(config, db);
}
}
@ -65,6 +71,44 @@ namespace Teknik.BillingService @@ -65,6 +71,44 @@ namespace Teknik.BillingService
return -1;
}
public static void SyncSubscriptions(Config config, TeknikEntities db)
{
// Get Biling Service
var billingService = BillingFactory.GetBillingService(config.BillingConfig);
foreach (var user in db.Users)
{
string email = UserHelper.GetUserEmailAddress(config, user.Username);
if (user.BillingCustomer != null)
{
// get the subscriptions for this user
var subscriptions = billingService.GetSubscriptionList(user.BillingCustomer.CustomerId);
var uploadPrice = subscriptions.SelectMany(s => s.Prices).FirstOrDefault(p => p.ProductId == config.BillingConfig.UploadProductId);
if (uploadPrice != null)
{
// Process Upload Settings
user.UploadSettings.MaxUploadStorage = uploadPrice.Storage;
user.UploadSettings.MaxUploadFileSize = uploadPrice.FileSize;
}
var emailPrice = subscriptions.SelectMany(s => s.Prices).FirstOrDefault(p => p.ProductId == config.BillingConfig.EmailProductId);
if (emailPrice != null)
{
UserHelper.EnableUserEmail(config, email);
UserHelper.EditUserEmailMaxSize(config, email, (int)emailPrice.Storage);
}
}
else
{
// No customer, so let's reset their info
user.UploadSettings.MaxUploadStorage = config.UploadConfig.MaxStorage;
user.UploadSettings.MaxUploadFileSize = config.UploadConfig.MaxUploadFileSize;
UserHelper.DisableUserEmail(config, email);
}
db.Entry(user).State = EntityState.Modified;
db.SaveChanges();
}
}
public static void Output(string message)
{
Console.WriteLine(message);

2
Configuration/BillingConfig.cs

@ -11,6 +11,7 @@ namespace Teknik.Configuration @@ -11,6 +11,7 @@ namespace Teknik.Configuration
public BillingType Type { get; set; }
public string StripePublishApiKey { get; set; }
public string StripeSecretApiKey { get; set; }
public string StripeWebhookSecret { get; set; }
public string UploadProductId { get; set; }
public string EmailProductId { get; set; }
@ -20,6 +21,7 @@ namespace Teknik.Configuration @@ -20,6 +21,7 @@ namespace Teknik.Configuration
Type = BillingType.Stripe;
StripePublishApiKey = null;
StripeSecretApiKey = null;
StripeWebhookSecret = null;
}
}
}

27
Configuration/UploadConfig.cs

@ -7,18 +7,12 @@ namespace Teknik.Configuration @@ -7,18 +7,12 @@ namespace Teknik.Configuration
{
public bool UploadEnabled { get; set; }
public bool DownloadEnabled { get; set; }
// Max upload size in bytes
public long MaxUploadSize { get; set; }
// Max Upload Size for basic users
public long MaxUploadSizeBasic { get; set; }
// Max Upload Size for premium users
public long MaxUploadSizePremium { get; set; }
// Gets the maximum download size before they are forced to the download page
public long MaxDownloadSize { get; set; }
// Maximum total size for basic users
public long MaxTotalSizeBasic { get; set; }
// Maximum total size for basic users
public long MaxTotalSizePremium { get; set; }
// Max upload size in bytes for free/anonymous users
public long MaxUploadFileSize { get; set; }
// Maximum file size before they are forced to the download page
public long MaxDownloadFileSize { get; set; }
// Maximum storage for free users
public long MaxStorage { get; set; }
public int UrlLength { get; set; }
public int DeleteKeyLength { get; set; }
public int KeySize { get; set; }
@ -45,12 +39,9 @@ namespace Teknik.Configuration @@ -45,12 +39,9 @@ namespace Teknik.Configuration
{
UploadEnabled = true;
DownloadEnabled = true;
MaxUploadSize = 100000000;
MaxUploadSizeBasic = 100000000;
MaxUploadSizePremium = 100000000;
MaxDownloadSize = 100000000;
MaxTotalSizeBasic = 1000000000;
MaxTotalSizePremium = 5000000000;
MaxUploadFileSize = 1073741824;
MaxDownloadFileSize = 1073741824;
MaxStorage = 5368709120;
UrlLength = 5;
DeleteKeyLength = 24;
KeySize = 256;

11
ServiceWorker/Program.cs

@ -163,15 +163,12 @@ namespace Teknik.ServiceWorker @@ -163,15 +163,12 @@ namespace Teknik.ServiceWorker
byte[] ivBytes = Encoding.UTF8.GetBytes(upload.IV);
long maxUploadSize = config.UploadConfig.MaxUploadSize;
long maxUploadSize = config.UploadConfig.MaxUploadFileSize;
if (upload.User != null)
{
maxUploadSize = config.UploadConfig.MaxUploadSizeBasic;
IdentityUserInfo userInfo = await IdentityHelper.GetIdentityUserInfo(config, upload.User.Username);
if (userInfo.AccountType == AccountType.Premium)
{
maxUploadSize = config.UploadConfig.MaxUploadSizePremium;
}
// Check account total limits
if (upload.User.UploadSettings.MaxUploadFileSize != null)
maxUploadSize = upload.User.UploadSettings.MaxUploadFileSize.Value;
}
using (AesCounterStream aesStream = new AesCounterStream(fileStream, false, keyBytes, ivBytes))

47
ServiceWorker/TeknikMigration.cs

@ -3,12 +3,15 @@ using System; @@ -3,12 +3,15 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Teknik.Areas.Paste;
using Teknik.Areas.Paste.Models;
using Teknik.Configuration;
using Teknik.Data;
using Teknik.StorageService;
using Teknik.Utilities;
using Teknik.Utilities.Cryptography;
namespace Teknik.ServiceWorker
{
@ -17,51 +20,9 @@ namespace Teknik.ServiceWorker @@ -17,51 +20,9 @@ namespace Teknik.ServiceWorker
public static bool RunMigration(TeknikEntities db, Config config)
{
bool success = false;
MigratePastes(db, config);
return success;
}
public static void MigratePastes(TeknikEntities db, Config config)
{
if (!Directory.Exists(config.PasteConfig.PasteDirectory))
{
Directory.CreateDirectory(config.PasteConfig.PasteDirectory);
}
var pastes = db.Pastes.Select(p => p.PasteId).ToList();
foreach (var pasteId in pastes)
{
var paste = db.Pastes.Where(p => p.PasteId == pasteId).FirstOrDefault();
if (!string.IsNullOrEmpty(paste.Content) && string.IsNullOrEmpty(paste.FileName) && string.IsNullOrEmpty(paste.HashedPassword))
{
// Generate a unique file name that does not currently exist
string filePath = FileHelper.GenerateRandomFileName(config.PasteConfig.PasteDirectory, config.PasteConfig.FileExtension, 10);
string fileName = Path.GetFileName(filePath);
string key = PasteHelper.GenerateKey(config.PasteConfig.KeySize);
string iv = PasteHelper.GenerateIV(config.PasteConfig.BlockSize);
// Encrypt the contents to the file
PasteHelper.EncryptContents(paste.Content, filePath, null, key, iv, config.PasteConfig.KeySize, config.PasteConfig.ChunkSize);
// Generate a deletion key
paste.DeleteKey = StringHelper.RandomString(config.PasteConfig.DeleteKeyLength);
paste.Key = key;
paste.KeySize = config.PasteConfig.KeySize;
paste.IV = iv;
paste.BlockSize = config.PasteConfig.BlockSize;
paste.FileName = fileName;
paste.Content = string.Empty;
db.Entry(paste).State = EntityState.Modified;
db.SaveChanges();
}
}
}
}
}

165
Teknik/App_Data/endpointMappings.json

@ -187,6 +187,116 @@ @@ -187,6 +187,116 @@
"action": "Index"
}
},
{
"Name": "Billing.Index",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "Index"
}
},
{
"Name": "Billing.Subscriptions",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "Subscriptions",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "ViewSubscriptions"
}
},
{
"Name": "Billing.ViewPaymentInfo",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "PaymentInfo",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "ViewPaymentInfo"
}
},
{
"Name": "Billing.Subscribe",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "Subscribe/{priceId}/{subscriptionId?}",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "Subscribe"
}
},
{
"Name": "Billing.Checkout",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "Checkout/{priceId}",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "Checkout"
}
},
{
"Name": "Billing.CheckoutComplete",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "CheckoutComplete",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "CheckoutComplete"
}
},
{
"Name": "Billing.EditSubscription",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "EditSubscription/{priceId}",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "EditSubscription"
}
},
{
"Name": "Billing.CancelSubscription",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "CancelSubscription/{subscriptionId}/{productId}",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "CancelSubscription"
}
},
{
"Name": "Billing.SubscriptionSuccess",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "SubscriptionSuccess/{priceId}",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "SubscriptionSuccess"
}
},
{
"Name": "Billing.Action",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "Action/{action}",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "Index"
}
},
{
"Name": "Blog.Blog",
"HostTypes": [ "Full" ],
@ -902,6 +1012,17 @@ @@ -902,6 +1012,17 @@
"action": "AccountSettings"
}
},
{
"Name": "User.BillingSettings",
"HostTypes": [ "Full" ],
"SubDomains": [ "account" ],
"Pattern": "Settings/Billing",
"Area": "User",
"Defaults": {
"controller": "User",
"action": "BillingSettings"
}
},
{
"Name": "User.SecuritySettings",
"HostTypes": [ "Full" ],
@ -1045,50 +1166,6 @@ @@ -1045,50 +1166,6 @@
"action": "ViewRawPGP"
}
},
{
"Name": "Billing.Index",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "Index"
}
},
{
"Name": "Billing.Subscriptions",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "Subscriptions",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "ViewSubscriptions"
}
},
{
"Name": "Billing.ViewPaymentInfo",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "PaymentInfo",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "ViewPaymentInfo"
}
},
{
"Name": "Billing.Action",
"HostTypes": [ "Full" ],
"SubDomains": [ "billing" ],
"Pattern": "Action/{action}",
"Area": "Billing",
"Defaults": {
"controller": "Billing",
"action": "Index"
}
},
{
"Name": "Vault.Index",
"HostTypes": [ "Full" ],

104
Teknik/Areas/API/V1/Controllers/BillingAPIv1Controller.cs

@ -0,0 +1,104 @@ @@ -0,0 +1,104 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Teknik.Areas.Users.Models;
using Teknik.Areas.Users.Utility;
using Teknik.BillingCore;
using Teknik.BillingCore.Models;
using Teknik.Configuration;
using Teknik.Data;
using Teknik.Logging;
namespace Teknik.Areas.API.V1.Controllers
{
public class BillingAPIv1Controller : APIv1Controller
{
public BillingAPIv1Controller(ILogger<Logger> logger, Config config, TeknikEntities dbContext) : base(logger, config, dbContext) { }
public async Task<IActionResult> HandleCheckoutCompleteEvent()
{
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);
var billingEvent = await billingService.ParseEvent(Request);
if (billingEvent == null)
return BadRequest();
var session = billingService.ProcessCheckoutCompletedEvent(billingEvent);
if (session.PaymentStatus == PaymentStatus.Paid)
{
var subscription = billingService.GetSubscription(session.SubscriptionId);
ProcessSubscription(session.CustomerId, subscription);
}
return Ok();
}
public async Task<IActionResult> HandleSubscriptionChange()
{
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);
var billingEvent = await billingService.ParseEvent(Request);
if (billingEvent == null)
return BadRequest();
var customerEvent = billingService.ProcessCustomerEvent(billingEvent);
foreach (var subscription in customerEvent.Subscriptions)
{
ProcessSubscription(customerEvent.CustomerId, subscription);
}
return Ok();
}
private void ProcessSubscription(string customerId, Subscription subscription)
{
// They have paid, so let's get their subscription and update their user settings
var user = _dbContext.Users.FirstOrDefault(u => u.BillingCustomer != null &&
u.BillingCustomer.CustomerId == customerId);
if (user != null)
{
var isActive = subscription.Status == SubscriptionStatus.Active;
foreach (var price in subscription.Prices)
{
ProcessPrice(user, price, isActive);
}
}
}
private void ProcessPrice(User user, Price price, bool active)
{
// What type of subscription is this
if (_config.BillingConfig.UploadProductId == price.ProductId)
{
// Process Upload Settings
user.UploadSettings.MaxUploadStorage = active ? price.Storage : _config.UploadConfig.MaxStorage;
user.UploadSettings.MaxUploadFileSize = active ? price.FileSize : _config.UploadConfig.MaxUploadFileSize;
_dbContext.Entry(user).State = EntityState.Modified;
_dbContext.SaveChanges();
}
else if (_config.BillingConfig.EmailProductId == price.ProductId)
{
// Process an email subscription
string email = UserHelper.GetUserEmailAddress(_config, user.Username);
if (active)
{
UserHelper.EnableUserEmail(_config, email);
UserHelper.EditUserEmailMaxSize(_config, email, (int)price.Storage);
}
else
{
UserHelper.DisableUserEmail(_config, email);
}
}
}
}
}

15
Teknik/Areas/API/V1/Controllers/UploadAPIv1Controller.cs

@ -40,22 +40,17 @@ namespace Teknik.Areas.API.V1.Controllers @@ -40,22 +40,17 @@ namespace Teknik.Areas.API.V1.Controllers
{
if (model.file != null)
{
long maxUploadSize = _config.UploadConfig.MaxUploadSize;
long maxUploadSize = _config.UploadConfig.MaxUploadFileSize;
long maxTotalSize = _config.UploadConfig.MaxUploadFileSize;
if (User.Identity.IsAuthenticated)
{
maxUploadSize = _config.UploadConfig.MaxUploadSizeBasic;
long maxTotalSize = _config.UploadConfig.MaxTotalSizeBasic;
IdentityUserInfo userInfo = await IdentityHelper.GetIdentityUserInfo(_config, User.Identity.Name);
if (userInfo.AccountType == AccountType.Premium)
{
maxUploadSize = _config.UploadConfig.MaxUploadSizePremium;
maxTotalSize = _config.UploadConfig.MaxTotalSizePremium;
}
// Check account total limits
var user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user.UploadSettings.MaxUploadStorage != null)
maxTotalSize = user.UploadSettings.MaxUploadStorage.Value;
if (user.UploadSettings.MaxUploadFileSize != null)
maxUploadSize = user.UploadSettings.MaxUploadFileSize.Value;
var userUploadSize = user.Uploads.Sum(u => u.ContentLength);
if (userUploadSize + model.file.Length > maxTotalSize)
{

121
Teknik/Areas/About/Controllers/AboutController.cs

@ -6,12 +6,16 @@ using Microsoft.AspNetCore.Authorization; @@ -6,12 +6,16 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Teknik.Areas.About.ViewModels;
using Teknik.Areas.Billing.ViewModels;
using Teknik.Areas.Users.Utility;
using Teknik.Attributes;
using Teknik.BillingCore;
using Teknik.Configuration;
using Teknik.Controllers;
using Teknik.Data;
using Teknik.Filters;
using Teknik.Logging;
using Teknik.Utilities.Routing;
namespace Teknik.Areas.About.Controllers
{
@ -27,8 +31,123 @@ namespace Teknik.Areas.About.Controllers @@ -27,8 +31,123 @@ namespace Teknik.Areas.About.Controllers
{
ViewBag.Title = "About";
ViewBag.Description = "What is Teknik?";
var vm = new AboutViewModel();
return View(new AboutViewModel());
// Get Biling Service
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);
var subVM = new SubscriptionsViewModel();
// Get current subscriptions
var curPrices = new Dictionary<string, List<string>>();
if (User.Identity.IsAuthenticated)
{
var user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user.BillingCustomer != null)
{
var currentSubs = billingService.GetSubscriptionList(user.BillingCustomer.CustomerId);
foreach (var curSub in currentSubs)
{
foreach (var price in curSub.Prices)
{
if (!curPrices.ContainsKey(price.ProductId))
curPrices[price.ProductId] = new List<string>();
curPrices[price.ProductId].Add(price.Id);
}
}
}
}
bool hasUploadProduct = curPrices.ContainsKey(_config.BillingConfig.UploadProductId);
bool hasEmailProduct = curPrices.ContainsKey(_config.BillingConfig.EmailProductId);
var curUploadPrice = string.Empty;
if (curPrices.ContainsKey(_config.BillingConfig.UploadProductId))
curUploadPrice = curPrices[_config.BillingConfig.UploadProductId].FirstOrDefault();
var curEmailPrice = string.Empty;
if (curPrices.ContainsKey(_config.BillingConfig.EmailProductId))
curEmailPrice = curPrices[_config.BillingConfig.EmailProductId].FirstOrDefault();
// Show Free Subscription
subVM.UploadSubscriptions.Add(new SubscriptionViewModel()
{
CurrentSubMonthly = !hasUploadProduct && User.Identity.IsAuthenticated,
SubscribeText = "Free",
SubscribeUrlMonthly = Url.SubRouteUrl("about", "About.Index"),
BaseStorage = _config.UploadConfig.MaxStorage
});
// Get Upload Prices
var uploadProduct = billingService.GetProduct(_config.BillingConfig.UploadProductId);
if (uploadProduct != null)
{
bool handledFirst = false;
foreach (var priceGrp in uploadProduct.Prices.GroupBy(p => p.Storage).OrderBy(p => p.Key))
{
// Get Monthly prices
var priceMonth = priceGrp.FirstOrDefault(p => p.Interval == BillingCore.Models.Interval.Month);
// Get Yearly prices
var priceYear = priceGrp.FirstOrDefault(p => p.Interval == BillingCore.Models.Interval.Year);
var curPrice = priceGrp.FirstOrDefault(p => p.Id == curUploadPrice);
subVM.UploadSubscriptions.Add(new SubscriptionViewModel()
{
Recommended = !handledFirst,
CurrentSubMonthly = curPrice?.Id == priceMonth?.Id,
CurrentSubYearly = curPrice?.Id == priceYear?.Id,
SubscribeUrlMonthly = Url.SubRouteUrl("billing",
hasUploadProduct ? "Billing.EditSubscription" : "Billing.Checkout",
new { priceId = priceMonth?.Id }),
SubscribeUrlYearly = Url.SubRouteUrl("billing",
hasUploadProduct ? "Billing.EditSubscription" : "Billing.Checkout",
new { priceId = priceYear?.Id }),
BaseStorage = priceMonth?.Storage,
BasePriceMonthly = priceMonth?.Amount,
BasePriceYearly = priceYear?.Amount
});
handledFirst = true;
}
}
// Get Email Prices
var emailProduct = billingService.GetProduct(_config.BillingConfig.EmailProductId);
if (emailProduct != null)
{
bool handledFirst = false;
foreach (var priceGrp in emailProduct.Prices.GroupBy(p => p.Storage).OrderBy(p => p.Key))
{
// Get Monthly prices
var priceMonth = priceGrp.FirstOrDefault(p => p.Interval == BillingCore.Models.Interval.Month);
// Get Yearly prices
var priceYear = priceGrp.FirstOrDefault(p => p.Interval == BillingCore.Models.Interval.Year);
var curPrice = priceGrp.FirstOrDefault(p => p.Id == curEmailPrice);
var emailSub = new SubscriptionViewModel()
{
Recommended = !handledFirst,
CurrentSubMonthly = curPrice?.Id == priceMonth?.Id,
CurrentSubYearly = curPrice?.Id == priceYear?.Id,
SubscribeUrlMonthly = Url.SubRouteUrl("billing",
hasEmailProduct ? "Billing.EditSubscription" : "Billing.Checkout",
new { priceId = priceMonth?.Id }),
SubscribeUrlYearly = Url.SubRouteUrl("billing",
hasEmailProduct ? "Billing.EditSubscription" : "Billing.Checkout",
new { priceId = priceYear?.Id }),
BaseStorage = priceMonth?.Storage,
BasePriceMonthly = priceMonth?.Amount,
BasePriceYearly = priceYear?.Amount
};
if (!handledFirst)
emailSub.PanelOffset = "3";
subVM.EmailSubscriptions.Add(emailSub);
handledFirst = true;
}
}
vm.Subscriptions = subVM;
return View(vm);
}
}
}

7
Teknik/Areas/About/ViewModels/AboutViewModel.cs

@ -2,11 +2,18 @@ @@ -2,11 +2,18 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.Areas.Billing.ViewModels;
using Teknik.ViewModels;
namespace Teknik.Areas.About.ViewModels
{
public class AboutViewModel : ViewModelBase
{
public SubscriptionsViewModel Subscriptions { get; set; }
public AboutViewModel()
{
Subscriptions = new SubscriptionsViewModel();
}
}
}

100
Teknik/Areas/About/Views/About/Index.cshtml

@ -1,8 +1,7 @@ @@ -1,8 +1,7 @@
@model Teknik.Areas.About.ViewModels.AboutViewModel
@{
string lifetimeAccount = "The account will not get deleted for inactivity as defined in the ToS";
string apiAccountAccess = "Passing user credentials allows linking for Uploads/Pastes/Shortened URLs and other account specific features";
@{
string extraUsage = string.Empty;
}
<div class="container">
@ -16,57 +15,35 @@ @@ -16,57 +15,35 @@
<p>
You can view our complete activity and statistics by visiting the <a href="@Url.SubRouteUrl("stats", "Stats.Index")">statistics</a> page.
</p>
<br />
<hr>
<hr />
<h2 class="text-center">What can you do with Teknik?</h2>
<br />
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<table class="table table-bordered">
<!-- Heading -->
<tr>
<th class="text-center"></th>
<th class="text-center"><a href="@Url.SubRouteUrl("account", "User.GetPremium")">Premium Account</a></th>
<th class="text-center"><a href="@Url.SubRouteUrl("account", "User.Register")">Basic Account</a></th>
<th class="text-center">No Account</th>
</tr>
<!-- Prices -->
<tr>
<td class="text-left"></td>
<td class="text-center">$@Config.UserConfig.PremiumAccountPrice @Config.UserConfig.PaymentType</td>
<td class="text-center">Free</td>
<td class="text-center">Free</td>
</tr>
<!-- Premium Features -->
<tr>
<td class="text-left"><span data-toggle="tooltip" data-placement="top" title="@lifetimeAccount">Lifetime Account <small>*</small></span></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-times fa-2x text-danger"></i></td>
<td class="text-center"><i class="fa fa-times fa-2x text-danger"></i></td>
</tr>
<tr>
<td class="text-left">Email Account</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-times fa-2x text-danger"></i></td>
<td class="text-center"><i class="fa fa-times fa-2x text-danger"></i></td>
<th class="text-center"><a href="@Url.SubRouteUrl("account", "User.Register")">Free Account</a></th>
<th class="text-center">Anonymous</th>
</tr>
<!-- Basic Features -->
<tr>
<td class="text-left">Git Repositories</td>
<td class="text-center">Unlimited Public &amp; Private Repos</td>
<td class="text-center">Unlimited Public &amp; Private Repos</td>
<td class="text-center"><i class="fa fa-times fa-2x text-danger"></i></td>
</tr>
<tr>