Browse Source

Added billing/subscriptions

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

+ 1
- 1
BillingCore/BillingFactory.cs View File

@@ -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)
{

+ 12
- 6
BillingCore/BillingService.cs View File

@@ -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
}

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

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
- 0
BillingCore/Models/CheckoutResult.cs View File

@@ -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
- 0
BillingCore/Models/CheckoutSession.cs View File

@@ -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
- 0
BillingCore/Models/Customer.cs View File

@@ -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
- 0
BillingCore/Models/Event.cs View File

@@ -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
- 0
BillingCore/Models/EventType.cs View File

@@ -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
- 0
BillingCore/Models/PaymentStatus.cs View File

@@ -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
- 0
BillingCore/Models/Price.cs View File

@@ -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
- 0
BillingCore/Models/Subscription.cs View File

@@ -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; }
}
}

+ 268
- 52
BillingCore/StripeService.cs View File

@@ -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
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

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
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

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

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
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
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
- 0
BillingService/Program.cs View File

@@ -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
if (options.SyncSubscriptions)
{
// Sync subscription information
SyncSubscriptions(config, db);
}
}

@@ -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
- 0
Configuration/BillingConfig.cs View File

@@ -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
Type = BillingType.Stripe;
StripePublishApiKey = null;
StripeSecretApiKey = null;
StripeWebhookSecret = null;
}
}
}

+ 9
- 18
Configuration/UploadConfig.cs View File

@@ -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
{
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;

+ 4
- 7
ServiceWorker/Program.cs View File

@@ -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))

+ 4
- 43
ServiceWorker/TeknikMigration.cs View File

@@ -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
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();
}
}
}
}
}

+ 121
- 44
Teknik/App_Data/endpointMappings.json View File

@@ -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 @@
"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 @@
"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
- 0
Teknik/Areas/API/V1/Controllers/BillingAPIv1Controller.cs View File

@@ -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);
}
}
}
}
}

+ 5
- 10
Teknik/Areas/API/V1/Controllers/UploadAPIv1Controller.cs View File

@@ -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)
{

+ 120
- 1
Teknik/Areas/About/Controllers/AboutController.cs View File

@@ -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
{
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
- 0
Teknik/Areas/About/ViewModels/AboutViewModel.cs View File

@@ -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();
}
}
}

+ 24
- 76
Teknik/Areas/About/Views/About/Index.cshtml View File

@@ -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 @@
<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>
<td class="text-left">Personal Blog</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></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>
</tr>
<tr>
<td class="text-left">Vault and Paste Editing</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></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>
</tr>
<tr>
@@ -81,101 +58,72 @@
</ul>
</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></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>
</tr>
<tr>
<td class="text-left"><span data-toggle="tooltip" data-placement="top">Service API</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-check fa-2x text-danger"></i></td>
</tr>
<!-- No Account Features -->
<tr>
<td class="text-left">Pastebin</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
</tr>
<tr>
<td class="text-left">File Uploads</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
</tr>
<tr>
<td class="text-left">Max Upload Filesize</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadSizePremium)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadSizeBasic)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadSize)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadFileSize)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadFileSize)</td>
</tr>
<tr>
<td class="text-left">Max Embedded Filesize</td>
<td class="text-center">Unlimited</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxDownloadSize)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxDownloadSize)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxDownloadFileSize)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxDownloadFileSize)</td>
</tr>
<tr>
<td class="text-left">Upload Storage</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxTotalSizePremium)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxTotalSizeBasic)</td>
<td class="text-center">@StringHelper.GetBytesReadable(Config.UploadConfig.MaxStorage)</td>
<td class="text-center">No Limit</td>
</tr>
<tr>
<td class="text-left">Upload Expiration</td>
<td class="text-center">Never</td>
<td class="text-center">Never</td>
<td class="text-center">24 Hours</td>
</tr>
<tr>
<td class="text-left">Url Shortening</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
</tr>
<tr>
<td class="text-left">Vault Creation</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
</tr>
<tr>
<td class="text-left">Technical Podcasts</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
</tr>
<tr>
<td class="text-left">Mumble Server</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
</tr>
<tr>
<td class="text-left">IRC Server</td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
</tr>
<tr>
<td class="text-left"><span data-toggle="tooltip" data-placement="top" title="@apiAccountAccess">Service API <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-check fa-2x text-success"></i></td>
<td class="text-center"><i class="fa fa-check fa-2x text-success"></i></td>
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<p>
<small>
* @lifetimeAccount
<br />
** @apiAccountAccess
</small>
</p>
</div>
</div>
<br />
@await Html.PartialAsync("../../Areas/Billing/Views/Billing/ViewSubscriptions", Model.Subscriptions);

<br />
<hr>

<h2 class="text-center">Want to help?</h2>
<br />
<p>
<p class="text-center">
Teknik's source code can be located on our <a href="https://git.teknik.io/Teknikode/">Git Repository</a> as well as all our internal tools and projects.
<br />
<br />

+ 188
- 29
Teknik/Areas/Billing/Controllers/BillingController.cs View File

@@ -1,11 +1,15 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.Areas.Billing.Models;
using Teknik.Areas.Billing.ViewModels;
using Teknik.Areas.Users.Models;
using Teknik.Areas.Users.Utility;
using Teknik.BillingCore;
using Teknik.Configuration;
using Teknik.Controllers;
@@ -15,6 +19,7 @@ using Teknik.Utilities.Routing;

namespace Teknik.Areas.Billing.Controllers
{
[Authorize]
[Area("Billing")]
public class BillingController : DefaultController
{
@@ -32,39 +37,47 @@ namespace Teknik.Areas.Billing.Controllers
var subVM = new SubscriptionsViewModel();

// Get Biling Service
var billingService = BillingFactory.GetStorageService(_config.BillingConfig);
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);

// Get current subscriptions
string curSubId = null;
var curSubs = new Dictionary<string, List<string>>();
var curPrices = new Dictionary<string, List<string>>();

if (User.Identity.IsAuthenticated)
{
var currentSubs = billingService.GetSubscriptionList(User.Identity.Name);
foreach (var curSub in currentSubs)
var user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user.BillingCustomer != null)
{
foreach (var price in curSub.Prices)
var currentSubs = billingService.GetSubscriptionList(user.BillingCustomer.CustomerId);
foreach (var curSub in currentSubs)
{
if (!curSubs.ContainsKey(price.ProductId))
curSubs[price.ProductId] = new List<string>();
curSubs[price.ProductId].Add(price.Id);
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()
{
CurrentPlan = curSubId == null,
CurrentSubMonthly = !hasUploadProduct && User.Identity.IsAuthenticated,
SubscribeText = "Free",
SubscribeUrlMonthly = Url.SubRouteUrl("account", "User.Register"),
BaseStorage = _config.UploadConfig.MaxUploadSizeBasic
SubscribeUrlMonthly = Url.SubRouteUrl("about", "About.Index"),
BaseStorage = _config.UploadConfig.MaxStorage
});

// Get Upload Prices
var curUploadSubs = new List<string>();
if (curSubs.ContainsKey(_config.BillingConfig.UploadProductId))
curUploadSubs = curSubs[_config.BillingConfig.UploadProductId];
var uploadProduct = billingService.GetProduct(_config.BillingConfig.UploadProductId);
if (uploadProduct != null)
{
@@ -77,13 +90,18 @@ namespace Teknik.Areas.Billing.Controllers
// Get Yearly prices
var priceYear = priceGrp.FirstOrDefault(p => p.Interval == BillingCore.Models.Interval.Year);

var isCurrent = curUploadSubs.Exists(s => priceGrp.FirstOrDefault(p => p.ProductId == s) != null);
var curPrice = priceGrp.FirstOrDefault(p => p.Id == curUploadPrice);
subVM.UploadSubscriptions.Add(new SubscriptionViewModel()
{
Recommended = !handledFirst,
CurrentPlan = isCurrent,
SubscribeUrlMonthly = Url.SubRouteUrl("billing", "Billing.Subscribe", new { priceId = priceMonth?.Id }),
SubscribeUrlYearly = Url.SubRouteUrl("billing", "Billing.Subscribe", new { priceId = priceYear?.Id }),
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
@@ -93,9 +111,6 @@ namespace Teknik.Areas.Billing.Controllers
}

// Get Email Prices
var curEmailSubs = new List<string>();
if (curSubs.ContainsKey(_config.BillingConfig.EmailProductId))
curEmailSubs = curSubs[_config.BillingConfig.EmailProductId];
var emailProduct = billingService.GetProduct(_config.BillingConfig.EmailProductId);
if (emailProduct != null)
{
@@ -108,13 +123,18 @@ namespace Teknik.Areas.Billing.Controllers
// Get Yearly prices
var priceYear = priceGrp.FirstOrDefault(p => p.Interval == BillingCore.Models.Interval.Year);

var isCurrent = curUploadSubs.Exists(s => priceGrp.FirstOrDefault(p => p.ProductId == s) != null);
var curPrice = priceGrp.FirstOrDefault(p => p.Id == curEmailPrice);
var emailSub = new SubscriptionViewModel()
{
Recommended = !handledFirst,
CurrentPlan = isCurrent,
SubscribeUrlMonthly = Url.SubRouteUrl("billing", "Billing.Subscribe", new { priceId = priceMonth?.Id }),
SubscribeUrlYearly = Url.SubRouteUrl("billing", "Billing.Subscribe", new { priceId = priceYear?.Id }),
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
@@ -136,13 +156,152 @@ namespace Teknik.Areas.Billing.Controllers
}

[AllowAnonymous]
public IActionResult Subscribe(string priceId)
public IActionResult Subscribe(string priceId, string subscriptionId)
{
// Get Subscription Info
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);

var vm = new SubscribeViewModel();
vm.Price = billingService.GetPrice(priceId);
vm.Subscription = billingService.GetSubscription(subscriptionId);

return View(vm);
}

public IActionResult Checkout(string priceId)
{
// Get Subscription Info
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);

var price = billingService.GetPrice(priceId);
if (price == null)
throw new ArgumentException("Invalid Price ID", "priceId");

User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user == null)
throw new UnauthorizedAccessException();

if (user.BillingCustomer == null)
{
var custId = billingService.CreateCustomer(user.Username, null);
var customer = new Customer()
{
CustomerId = custId,
User = user
};
_dbContext.Customers.Add(customer);
user.BillingCustomer = customer;
_dbContext.Entry(user).State = EntityState.Modified;
_dbContext.SaveChanges();
}

var session = billingService.CreateCheckoutSession(user.BillingCustomer.CustomerId,
priceId,
Url.SubRouteUrl("billing", "Billing.CheckoutComplete", new { productId = price.ProductId }),
Url.SubRouteUrl("billing", "Billing.Subscriptions"));
return Redirect(session.Url);
}

public IActionResult CheckoutComplete(string productId, string session_id)
{
// Get Checkout Info
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);
var checkoutSession = billingService.GetCheckoutSession(session_id);
if (checkoutSession != null)
{
var subscription = billingService.GetSubscription(checkoutSession.SubscriptionId);
if (subscription != null)
{
foreach (var price in subscription.Prices)
{
if (price.ProductId == productId)
{
return Redirect(Url.SubRouteUrl("billing", "Billing.SubscriptionSuccess", new { priceId = price.Id }));
}
}
}
}

return Redirect(Url.SubRouteUrl("billing", "Billing.ViewSubscriptions"));
}

public IActionResult EditSubscription(string priceId)
{
// Get Subscription Info
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);

var price = billingService.GetPrice(priceId);
if (price == null)
throw new ArgumentException("Invalid Price ID", "priceId");

User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user == null)
throw new UnauthorizedAccessException();
if (user.BillingCustomer == null)
{
return Checkout(priceId);
}
else
{
var currentSubs = billingService.GetSubscriptionList(user.BillingCustomer.CustomerId);
foreach (var curSub in currentSubs)
{
if (curSub.Prices.Exists(p => p.ProductId == price.ProductId))
{
billingService.EditSubscriptionPrice(curSub.Id, price.Id);
return Redirect(Url.SubRouteUrl("billing", "Billing.SubscriptionSuccess", new { priceId = price.Id }));
}
}
}

return Redirect(Url.SubRouteUrl("billing", "Billing.ViewSubscriptions"));
}

public IActionResult CancelSubscription(string subscriptionId, string productId)
{
// Get Subscription Info
var billingService = BillingFactory.GetStorageService(_config.BillingConfig);
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);

var subscription = billingService.GetSubscription(subscriptionId);
if (subscription == null)
throw new ArgumentException("Invalid Subscription Id", "subscriptionId");

if (!subscription.Prices.Exists(p => p.ProductId == productId))
throw new ArgumentException("Subscription does not relate to product", "productId");

var product = billingService.GetProduct(productId);
if (product == null)
throw new ArgumentException("Product does not exist", "productId");

var result = billingService.CancelSubscription(subscriptionId);

var vm = new CancelSubscriptionViewModel()
{
ProductName = product.Name
};

return View(vm);
}

public IActionResult SubscriptionSuccess(string priceId)
{
var vm = new SubscriptionSuccessViewModel();

// Get Subscription Info
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);

var price = billingService.GetPrice(priceId);
if (price == null)
throw new ArgumentException("Invalid Price ID", "priceId");

var product = billingService.GetProduct(price.ProductId);
vm.ProductName = product.Name;
vm.Price = price.Amount;
vm.Interval = price.Interval.ToString();
vm.Storage = price.Storage;

return View(new SubscriptionViewModel());
return View(vm);
}
}
}

+ 19
- 0
Teknik/Areas/Billing/Models/Customer.cs View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.Areas.Users.Models;
using Teknik.Attributes;

namespace Teknik.Areas.Billing.Models
{
public class Customer
{
[CaseSensitive]
public string CustomerId { get; set; }

public int UserId { get; set; }

public virtual User User { get; set; }
}
}

+ 13
- 0
Teknik/Areas/Billing/ViewModels/CancelSubscriptionViewModel.cs View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.ViewModels;

namespace Teknik.Areas.Billing.ViewModels
{
public class CancelSubscriptionViewModel : ViewModelBase
{
public string ProductName { get; set; }
}
}

+ 17
- 0
Teknik/Areas/Billing/ViewModels/PriceViewModel.cs View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.ViewModels;

namespace Teknik.Areas.Billing.ViewModels
{
public class PriceViewModel : ViewModelBase
{
public string ProductName { get; set; }

public string Name { get; set; }

public long Storage { get; set; }
}
}

+ 15
- 0
Teknik/Areas/Billing/ViewModels/SubscribeViewModel.cs View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.BillingCore.Models;
using Teknik.ViewModels;

namespace Teknik.Areas.Billing.ViewModels
{
public class SubscribeViewModel : ViewModelBase
{
public Subscription Subscription { get; set; }
public Price Price { get; set; }
}
}

+ 20
- 0
Teknik/Areas/Billing/ViewModels/SubscriptionSuccessViewModel.cs View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.BillingCore.Models;
using Teknik.ViewModels;

namespace Teknik.Areas.Billing.ViewModels
{
public class SubscriptionSuccessViewModel : ViewModelBase
{
public string ProductName { get; set; }

public decimal? Price { get; set; }

public string Interval { get; set; }

public long Storage { get; set; }
}
}

+ 4
- 2
Teknik/Areas/Billing/ViewModels/SubscriptionViewModel.cs View File

@@ -9,7 +9,8 @@ namespace Teknik.Areas.Billing.ViewModels
public class SubscriptionViewModel : ViewModelBase
{
public bool Recommended { get; set; }
public bool CurrentPlan { get; set; }
public bool CurrentSubMonthly { get; set; }
public bool CurrentSubYearly { get; set; }
public string SubscriptionId { get; set; }
public decimal? BasePriceMonthly { get; set; }
public decimal? BasePriceYearly { get; set; }
@@ -28,7 +29,8 @@ namespace Teknik.Areas.Billing.ViewModels
public SubscriptionViewModel()
{
Recommended = false;
CurrentPlan = false;
CurrentSubMonthly = false;
CurrentSubYearly = false;
OverageAllowed = false;
}
}

+ 21
- 0
Teknik/Areas/Billing/Views/Billing/CancelSubscription.cshtml View File

@@ -0,0 +1,21 @@
@model Teknik.Areas.Billing.ViewModels.CancelSubscriptionViewModel

<div class="container">
@if (Model.Error)
{
<div class="row">
<div class="col-sm-12 text-center">
<h2>@Model.ErrorMessage</h2>
</div>
</div>
}
else
{
<div class="row">
<div class="col-md-12 text-center">
<h2>You have successfully canceled your subscription to <strong>@Model.ProductName</strong></h2>
<p><a href="@Url.SubRouteUrl("account", "User.BillingSettings")">View Active Subscriptions</a></p>
</div>
</div>
}
</div>

+ 45
- 0
Teknik/Areas/Billing/Views/Billing/Subscribe.cshtml View File

@@ -0,0 +1,45 @@
@model Teknik.Areas.Billing.ViewModels.SubscribeViewModel

@{
var price = Model.Price.Amount != null ? $"${Model.Price.Amount:0.00}" : "Free";
var interval = Model.Price.Interval.ToString();
var priceText = Model.Price.Amount != null ? $"{price}/{interval}" : "Free";
var storageText = $"{StringHelper.GetBytesReadable(Model.Price.Storage)}";

if (Model.Subscription != null)
{
}
}

<div class="container">
<div class="row">
<div class="col-sm-12">
<div id="subscribeStatus">
@if (Model.Error)
{
<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>@Model.ErrorMessage</div>
}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-5 col-sm-offset-1 text-left">@storageText</div>
<div class="col-sm-5 text-right text-primary">@priceText</div>
</div>
<div class="row">
<div class="col-sm-5 col-sm-offset-6 text-right text-muted">no tax</div>
</div>
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<p><strong>You'll be charged @price automatically every @interval until you cancel. Your price may change as described in the <a href="@Url.SubRouteUrl("tos", "TOS.Index")" target="_blank">Teknik Terms of Service</a>.</strong></p>
<p>Cancel anytime in your <a href="@Url.SubRouteUrl("account", "Account.Subscriptions")" target="_blank">Subscriptions Settings</a>.</p>
<p></p>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<button class="btn btn-default" id="subscribeCancel" type="button" name="Subscription.Cancel">Cancel</button>
<a href="@Url.SubRouteUrl("billing", "Billing.Checkout", new { priceId = Model.Price.Id, subscriptionId = Model.Subscription?.Id })" class="btn btn-primary" role="button">Subscribe</a>
</div>
</div>
</div>

+ 21
- 0
Teknik/Areas/Billing/Views/Billing/SubscriptionSuccess.cshtml View File

@@ -0,0 +1,21 @@
@model Teknik.Areas.Billing.ViewModels.SubscriptionSuccessViewModel

<div class="container">
@if (Model.Error)
{
<div class="row">
<div class="col-sm-12 text-center">
<h2>@Model.ErrorMessage</h2>
</div>
</div>
}
else
{
<div class="row">
<div class="col-md-12 text-center">
<h2>Thank you for subscribing to @Model.ProductName: <strong>@(StringHelper.GetBytesReadable(Model.Storage))</strong> for <strong>@($"${Model.Price:0.00} / {Model.Interval}")</strong></h2>
<p><a href="@Url.SubRouteUrl("account", "User.BillingSettings")">View Active Subscriptions</a></p>
</div>
</div>
}
</div>

+ 10
- 3
Teknik/Areas/Billing/Views/Billing/ViewSubscription.cshtml View File

@@ -13,10 +13,10 @@

var btnClass = "";
var subscribeText = Model.BasePriceMonthly.HasValue ? $"${Model.BasePriceMonthly:0.00} / month" : "Free";
if (Model.CurrentPlan)
if (Model.CurrentSubMonthly)
{