1
0
mirror of https://git.teknik.io/Teknikode/Teknik.git synced 2023-08-02 14:16:22 +02:00

Added billing/subscriptions

This commit is contained in:
Uncled1023 2021-11-13 20:28:24 -08:00
parent 63ef371a4a
commit 543df28eb7
57 changed files with 4068 additions and 510 deletions

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

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

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

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

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

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

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

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

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

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

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 priceService = new PriceService();
var priceList = priceService.List(options);
if (priceList != null)
{
foreach (var price in priceList)
var options = new PriceListOptions
{
foundPrices.Add(ConvertPrice(price));
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)
{
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>
{
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
};
}
}

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

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

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;

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

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

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" ],

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

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

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

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

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>
<th class="text-center"><a href="@Url.SubRouteUrl("account", "User.Register")">Free Account</a></th>
<th class="text-center">Anonymous</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>
</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 />

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.GetStorageService(_config.BillingConfig);
var price = billingService.GetPrice(priceId);
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);
return View(new SubscriptionViewModel());
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.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(vm);
}
}
}

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

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

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

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

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

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

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>

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>

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>

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)
{
btnClass = "disabled";
subscribeText = "Current plan";
subscribeText = "Current Plan";
}
var panelColor = "default";
@ -53,12 +53,19 @@
<p><a href="@Model.SubscribeUrlMonthly" class="btn btn-@buttonColor center-block @btnClass" role="button">@subscribeText</a></p>
@if (Model.BasePriceYearly != null)
{
var yearBtnClass = "";
var yearSubscribeText = $"${Model.BasePriceYearly:0.00} / year";
if (Model.CurrentSubYearly)
{
yearBtnClass = "disabled";
yearSubscribeText = "Current Plan";
}
var discount = 1 - (Model.BasePriceYearly.Value / (Model.BasePriceMonthly.Value * 12));
<p>
Or prepay annually<br />
(save @Math.Round(100 * discount)%):
</p>
<p><a href="@Model.SubscribeUrlYearly" class="btn btn-default btn-no-border btn-text-primary center-block" role="button">@($"${Model.BasePriceYearly:0.00}") / year</a></p>
<p><a href="@Model.SubscribeUrlYearly" class="btn btn-default btn-no-border btn-text-primary center-block @yearBtnClass" role="button">@yearSubscribeText</a></p>
}
</div>
</div>

View File

@ -8,10 +8,7 @@
@if (Model.UploadSubscriptions.Any())
{
<h2 class="text-center">Upload Storage Subscriptions</h2>
<p class="text-muted text-center">
By subscribing to a Teknik Service plan, you agree to the <a href="@Url.SubRouteUrl("tos", "TOS.Index")">Teknik Terms of Service</a>.<br />
<strong>Note:</strong> The <a href="@Url.SubRouteUrl("privacy", "Privacy.Index")">Teknik Privacy Policy</a> describes how data is handled in this service.
</p>
<p class="text-center">Want some extra storage? Upgrade with one of the storage subscriptions!</p>
<br />
<div class="row">
@foreach (var subVM in Model.UploadSubscriptions)
@ -22,24 +19,13 @@
@await Html.PartialAsync("../../Areas/Billing/Views/Billing/ViewSubscription", subVM)
}
</div>
@if (!string.IsNullOrEmpty(extraUsage))
{
<div class="row">
<div class="col-sm-12">
<p>
<small>
* @extraUsage
</small>
</p>
</div>
</div>
}
}
@if (Model.EmailSubscriptions.Any())
{
<br />
<h2 class="text-center">Email Subscriptions</h2>
<p class="text-center">Get a personal email address, complete with web mail client.</p>
<br />
<div class="row">
@foreach (var subVM in Model.EmailSubscriptions)
@ -51,4 +37,25 @@
}
</div>
}
</div>
<br />
<div class="row">
<div class="col-sm-12">
<p class="text-muted text-center">
By subscribing to a Teknik Service plan, you agree to the <a href="@Url.SubRouteUrl("tos", "TOS.Index")" target="_blank">Teknik Terms of Service</a>.<br />
<strong>Note:</strong> The <a href="@Url.SubRouteUrl("privacy", "Privacy.Index")" target="_blank">Teknik Privacy Policy</a> describes how data is handled in this service.
</p>
@if (!string.IsNullOrEmpty(extraUsage))
{
<p>
<small>
* @extraUsage
</small>
</p>
}
</div>
</div>
</div>
<bundle src="js/billing.min.js" append-version="true"></bundle>

View File

@ -119,7 +119,7 @@
</div>
<div id="collapse200" class="panel-collapse collapse" role="tabpanel">
<div class="panel-body">
The max file size for everyone is <strong>@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadSize)</strong>, basic accounts are <strong>@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadSizeBasic)</strong>, and Premium accounts are <strong>@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadSizePremium)</strong>.
The max file size for everyone is <strong>@StringHelper.GetBytesReadable(Config.UploadConfig.MaxUploadFileSize)</strong>.
</div>
</div>
</div>

View File

@ -118,18 +118,14 @@ namespace Teknik.Areas.Help.Controllers
ViewBag.Title = "Upload Service Help";
UploadHelpViewModel model = new UploadHelpViewModel();
model.MaxUploadSize = _config.UploadConfig.MaxUploadSize;
model.MaxUploadSize = _config.UploadConfig.MaxUploadFileSize;
if (User.Identity.IsAuthenticated)
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user != null)
{
model.MaxUploadSize = _config.UploadConfig.MaxUploadSizeBasic;
IdentityUserInfo userInfo = await IdentityHelper.GetIdentityUserInfo(_config, User.Identity.Name);
if (userInfo.AccountType == AccountType.Premium)
{
model.MaxUploadSize = _config.UploadConfig.MaxUploadSizePremium;
}
if (user.UploadSettings.MaxUploadFileSize != null)
model.MaxUploadSize = user.UploadSettings.MaxUploadFileSize.Value;
}
}
return View("~/Areas/Help/Views/Help/Upload.cshtml", model);

View File

@ -131,7 +131,7 @@
</div>
<br />
<hr />
<h2 class="text-center"><a href="@Url.SubRouteUrl("account", "User.GetPremium")">Upgrade to Premium</a> for even more features!</h2>
<h2 class="text-center"><a href="@Url.SubRouteUrl("billing", "Billing.Subscriptions")">Subscribe</a> for even more features!</h2>
<div class="row">
<a conditional href="@Url.SubRouteUrl("help", "Help.Mail")" asp-condition="@User.Identity.IsAuthenticated">
<div class="col-md-3 text-center text-primary">
@ -154,18 +154,18 @@
<a conditional href="@Url.SubRouteUrl("upload", "Upload.Index")" asp-condition="@User.Identity.IsAuthenticated">
<div class="col-md-3 text-center text-primary">
<br />
<i class="fa fa-film fa-5x"></i>
<i class="fa fa-hdd-o fa-5x"></i>
<div class="caption">
<h3>No Embed Limits</h3>
<h3>Additional Storage</h3>
</div>
</div>
</a>
<a conditional href="@Url.SubRouteUrl("about", "About.Index")" asp-condition="@User.Identity.IsAuthenticated">
<a conditional href="@Url.SubRouteUrl("upload", "Upload.Index")" asp-condition="@User.Identity.IsAuthenticated">
<div class="col-md-3 text-center text-primary">
<br />
<i class="fa fa-heart fa-5x"></i>
<i class="fa fa-film fa-5x"></i>
<div class="caption">
<h3>Lifetime Account</h3>
<h3>No Embed Limits</h3>
</div>
</div>
</a>

View File

@ -93,7 +93,7 @@ namespace Teknik.Areas.Paste.Controllers
string hash = string.Empty;
if (!string.IsNullOrEmpty(password))
{
hash = PasteHelper.HashPassword(paste.Key, password);
hash = Crypto.HashPassword(paste.Key, password);
keyBytes = AesCounterManaged.CreateKey(password, ivBytes, paste.KeySize);
}
if (string.IsNullOrEmpty(password) || hash != paste.HashedPassword)
@ -242,7 +242,7 @@ namespace Teknik.Areas.Paste.Controllers
string hash = string.Empty;
if (!string.IsNullOrEmpty(password))
{
hash = PasteHelper.HashPassword(paste.Key, password);
hash = Crypto.HashPassword(paste.Key, password);
keyBytes = AesCounterManaged.CreateKey(password, ivBytes, paste.KeySize);
}
if (string.IsNullOrEmpty(password) || hash != paste.HashedPassword)
@ -307,7 +307,7 @@ namespace Teknik.Areas.Paste.Controllers
string hash = string.Empty;
if (!string.IsNullOrEmpty(password))
{
hash = PasteHelper.HashPassword(paste.Key, password);
hash = Crypto.HashPassword(paste.Key, password);
}
if (string.IsNullOrEmpty(password) || hash != paste.HashedPassword)
{
@ -333,8 +333,8 @@ namespace Teknik.Areas.Paste.Controllers
// Generate a unique file name that does not currently exist
string fileName = storageService.GetUniqueFileName();
string key = PasteHelper.GenerateKey(_config.PasteConfig.KeySize);
string iv = PasteHelper.GenerateIV(_config.PasteConfig.BlockSize);
string key = Crypto.GenerateKey(_config.PasteConfig.KeySize);
string iv = Crypto.GenerateIV(_config.PasteConfig.BlockSize);
PasteHelper.EncryptContents(storageService, model.Content, fileName, password, key, iv, _config.PasteConfig.KeySize, _config.PasteConfig.ChunkSize);
@ -344,7 +344,7 @@ namespace Teknik.Areas.Paste.Controllers
paste.BlockSize = _config.PasteConfig.BlockSize;
if (!string.IsNullOrEmpty(password))
paste.HashedPassword = PasteHelper.HashPassword(paste.Key, password);
paste.HashedPassword = Crypto.HashPassword(paste.Key, password);
paste.FileName = fileName;
paste.Title = model.Title;
paste.Syntax = model.Syntax;

View File

@ -59,15 +59,14 @@ namespace Teknik.Areas.Paste
break;
}
string key = GenerateKey(config.PasteConfig.KeySize);
string iv = GenerateIV(config.PasteConfig.BlockSize);
string key = Crypto.GenerateKey(config.PasteConfig.KeySize);
string iv = Crypto.GenerateIV(config.PasteConfig.BlockSize);
if (!string.IsNullOrEmpty(password))
{
paste.HashedPassword = HashPassword(key, password);
paste.HashedPassword = Crypto.HashPassword(key, password);
}
// Generate a unique file name that does not currently exist
var storageService = StorageServiceFactory.GetStorageService(config.PasteConfig.StorageConfig);
var fileName = storageService.GetUniqueFileName();
@ -102,21 +101,6 @@ namespace Teknik.Areas.Paste
return false;
}
public static string GenerateKey(int keySize)
{
return StringHelper.RandomString(keySize / 8);
}
public static string GenerateIV(int ivSize)
{
return StringHelper.RandomString(ivSize / 16);
}
public static string HashPassword(string key, string password)
{
return SHA384.Hash(key, password).ToHex();
}
public static void EncryptContents(IStorageService storageService, string content, string fileName, string password, string key, string iv, int keySize, int chunkSize)
{
byte[] ivBytes = Encoding.Unicode.GetBytes(iv);

View File

@ -45,7 +45,7 @@ namespace Teknik.Areas.Upload.Controllers
model.Encrypt = false;
model.ExpirationLength = 1;
model.ExpirationUnit = ExpirationUnit.Days;
model.MaxUploadSize = _config.UploadConfig.MaxUploadSize;
model.MaxUploadSize = _config.UploadConfig.MaxUploadFileSize;
if (User.Identity.IsAuthenticated)
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
@ -58,16 +58,12 @@ namespace Teknik.Areas.Upload.Controllers
model.CurrentTotalSize = user.Uploads.Sum(u => u.ContentLength);
model.MaxUploadSize = _config.UploadConfig.MaxUploadSizeBasic;
model.MaxTotalSize = _config.UploadConfig.MaxTotalSizeBasic;
IdentityUserInfo userInfo = await IdentityHelper.GetIdentityUserInfo(_config, User.Identity.Name);
if (userInfo.AccountType == AccountType.Premium)
{
model.MaxUploadSize = _config.UploadConfig.MaxUploadSizePremium;
model.MaxTotalSize = _config.UploadConfig.MaxTotalSizePremium;
}
model.MaxUploadSize = _config.UploadConfig.MaxUploadFileSize;
model.MaxTotalSize = _config.UploadConfig.MaxStorage;
if (user.UploadSettings.MaxUploadStorage != null)
model.MaxTotalSize = user.UploadSettings.MaxUploadStorage.Value;
if (user.UploadSettings.MaxUploadFileSize != null)
model.MaxUploadSize = user.UploadSettings.MaxUploadFileSize.Value;
if (model.CurrentTotalSize >= model.MaxTotalSize)
{
@ -88,22 +84,17 @@ namespace Teknik.Areas.Upload.Controllers
{
if (_config.UploadConfig.UploadEnabled)
{
long maxUploadSize = _config.UploadConfig.MaxUploadSize;
long maxUploadSize = _config.UploadConfig.MaxUploadFileSize;
long maxTotalSize = _config.UploadConfig.MaxStorage;
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 + uploadFile.file.Length > maxTotalSize)
{
@ -232,7 +223,6 @@ namespace Teknik.Areas.Upload.Controllers
string iv = string.Empty;
string contentType = string.Empty;
long contentLength = 0;
bool premiumAccount = false;
DateTime dateUploaded = new DateTime();
Models.Upload upload = _dbContext.Uploads.Where(up => up.Url == file).FirstOrDefault();
@ -256,16 +246,6 @@ namespace Teknik.Areas.Upload.Controllers
contentType = upload.ContentType;
contentLength = upload.ContentLength;
dateUploaded = upload.DateUploaded;
if (User.Identity.IsAuthenticated)
{
IdentityUserInfo userInfo = await IdentityHelper.GetIdentityUserInfo(_config, User.Identity.Name);
premiumAccount = userInfo.AccountType == AccountType.Premium;
}
if (!premiumAccount && upload.UserId != null)
{
IdentityUserInfo userInfo = await IdentityHelper.GetIdentityUserInfo(_config, upload.User.Username);
premiumAccount = userInfo.AccountType == AccountType.Premium;
}
}
else
{
@ -285,7 +265,7 @@ namespace Teknik.Areas.Upload.Controllers
return View(model);
}
else if (!premiumAccount && _config.UploadConfig.MaxDownloadSize < contentLength)
else if (_config.UploadConfig.MaxDownloadFileSize < contentLength)
{
// We want to force them to the dl page due to them being over the max download size for embedded content
DownloadViewModel model = new DownloadViewModel();

View File

@ -37,6 +37,7 @@ using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Http;
using IdentityServer4.Models;
using Teknik.Utilities.Routing;
using Teknik.BillingCore;
namespace Teknik.Areas.Users.Controllers
{
@ -357,6 +358,51 @@ namespace Teknik.Areas.Users.Controllers
return new StatusCodeResult(StatusCodes.Status403Forbidden);
}
[TrackPageView]
public IActionResult BillingSettings()
{
string username = User.Identity.Name;
User user = UserHelper.GetUser(_dbContext, username);
if (user != null)
{
ViewBag.Title = "Billing Settings";
ViewBag.Description = "Your " + _config.Title + " Billing Settings";
BillingSettingsViewModel model = new BillingSettingsViewModel();
model.Page = "Billing";
model.UserID = user.UserId;
model.Username = user.Username;
if (user.BillingCustomer != null)
{
var billingService = BillingFactory.GetBillingService(_config.BillingConfig);
var subs = billingService.GetSubscriptionList(user.BillingCustomer.CustomerId);
foreach (var sub in subs)
{
foreach (var price in sub.Prices)
{
var product = billingService.GetProduct(price.ProductId);
var subView = new SubscriptionViewModel()
{
SubscriptionId = sub.Id,
ProductId = product.ProductId,
ProductName = product.Name,
Storage = price.Storage,
Price = price.Amount,
Interval = price.Interval.ToString()
};
model.Subscriptions.Add(subView);
}
}
}
return View("/Areas/User/Views/User/Settings/BillingSettings.cshtml", model);
}
return new StatusCodeResult(StatusCodes.Status403Forbidden);
}
[TrackPageView]
public async Task<IActionResult> SecuritySettings()
{

View File

@ -21,14 +21,20 @@ namespace Teknik.Areas.Users.Models
public ExpirationUnit ExpirationUnit { get; set; }
[Column("MaxUploadStorage")]
// Maximum Allotted total upload storage
public long? MaxUploadStorage { get; set; }
[Column("MaxUploadFileSize")]
// Maximum allowed file size
public long? MaxUploadFileSize { get; set; }
public UploadSettings()
{
Encrypt = false;
ExpirationLength = 1;
ExpirationUnit = ExpirationUnit.Never;
MaxUploadStorage = null;
MaxUploadFileSize = null;
}
}
}

View File

@ -6,6 +6,7 @@ using Teknik.Attributes;
using Teknik.Models;
using Teknik.Utilities;
using Microsoft.AspNetCore.Identity;
using Teknik.Areas.Billing.Models;
namespace Teknik.Areas.Users.Models
{
@ -15,6 +16,8 @@ namespace Teknik.Areas.Users.Models
public string Username { get; set; }
public virtual Customer BillingCustomer { get; set; }
public virtual ICollection<UserFeature> Features { get; set; }
public virtual InviteCode ClaimedInviteCode { get; set; }

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.BillingCore.Models;
namespace Teknik.Areas.Users.ViewModels
{
public class BillingSettingsViewModel : SettingsViewModel
{
public List<SubscriptionViewModel> Subscriptions { get; set; }
public BillingSettingsViewModel()
{
Subscriptions = new List<SubscriptionViewModel>();
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.ViewModels;
namespace Teknik.Areas.Users.ViewModels
{
public class SubscriptionViewModel : SettingsViewModel
{
public string SubscriptionId { get; set; }
public string ProductId { get; set; }
public string ProductName { get; set; }
public decimal? Price { get; set; }
public string Interval { get; set; }
public long Storage { get; set; }
}
}

View File

@ -0,0 +1,32 @@
@model Teknik.Areas.Users.ViewModels.BillingSettingsViewModel
@using Teknik.BillingCore.Models
@{
Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml";
}
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<br />
<label for="activeSubscriptions"><h4>Active Subscriptions</h4></label>
<div id="activeSubscriptions" style="overflow-y: auto; max-height: 400px;">
<ul class="list-group" id="activeSubscriptionList">
@if (Model.Subscriptions.Any())
{
foreach (var subscription in Model.Subscriptions)
{
<li class="list-group-item">
<h4 class="list-group-item-heading">@subscription.ProductName: <strong>@(StringHelper.GetBytesReadable(subscription.Storage))</strong> for <strong>@($"${subscription.Price:0.00} / {subscription.Interval}")</strong></h4>
<p><a href="@(Url.SubRouteUrl("billing", "Billing.CancelSubscription", new { subscriptionId = subscription.SubscriptionId, productId = subscription.ProductId }))">Cancel Subscription</a></p>
</li>
}
}
else
{
<li class="list-group-item text-center">No Active Subscriptions</li>
}
</ul>
</div>
</div>
</div>

View File

@ -17,6 +17,7 @@
<div class="panel-heading text-center"><strong>Personal Settings</strong></div>
<a href="@Url.SubRouteUrl("account", "User.ProfileSettings")" class="list-group-item @(Model.Page == "Profile" ? "active" : string.Empty)">Profile</a>
<a href="@Url.SubRouteUrl("account", "User.AccountSettings")" class="list-group-item @(Model.Page == "Account" ? "active" : string.Empty)">Account</a>
<a href="@Url.SubRouteUrl("account", "User.BillingSettings")" class="list-group-item @(Model.Page == "Billing" ? "active" : string.Empty)">Billing</a>
<a href="@Url.SubRouteUrl("account", "User.SecuritySettings")" class="list-group-item @(Model.Page == "Security" ? "active" : string.Empty)">Security</a>
<a href="@Url.SubRouteUrl("account", "User.InviteSettings")" class="list-group-item @(Model.Page == "Invite" ? "active" : string.Empty)">Invite Codes</a>
<a href="@Url.SubRouteUrl("account", "User.BlogSettings")" class="list-group-item @(Model.Page == "Blog" ? "active" : string.Empty)">Blogging</a>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Teknik.Data.Migrations
{
public partial class AddBillingCustomers : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Customers",
columns: table => new
{
CustomerId = table.Column<string>(type: "nvarchar(450)", nullable: false),
UserId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Customers", x => x.CustomerId);
table.ForeignKey(
name: "FK_Customers_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Customers_UserId",
table: "Customers",
column: "UserId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Customers");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Teknik.Data.Migrations
{
public partial class RenameUploadSettings : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "MaxUploadFileSize",
table: "Users",
type: "bigint",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MaxUploadFileSize",
table: "Users");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ using Teknik.Areas.Vault.Models;
using Microsoft.EntityFrameworkCore;
using Teknik.Models;
using System.Configuration;
using Teknik.Areas.Billing.Models;
namespace Teknik.Data
{
@ -25,6 +26,8 @@ namespace Teknik.Data
public DbSet<UserSettings> UserSettings { get; set; }
public DbSet<BlogSettings> BlogSettings { get; set; }
public DbSet<UploadSettings> UploadSettings { get; set; }
// Billing Customers
public DbSet<Customer> Customers { get; set; }
// Features
public DbSet<Feature> Features { get; set; }
// Blogs
@ -72,6 +75,7 @@ namespace Teknik.Data
modelBuilder.Entity<User>().OwnsOne(u => u.UserSettings, us => us.ToTable("Users"));
modelBuilder.Entity<User>().OwnsOne(u => u.BlogSettings, bs => bs.ToTable("Users"));
modelBuilder.Entity<User>().OwnsOne(u => u.UploadSettings, us => us.ToTable("Users"));
modelBuilder.Entity<User>().HasOne(u => u.BillingCustomer).WithOne(i => i.User);
modelBuilder.Entity<User>().HasMany(u => u.Features).WithOne(u => u.User);
modelBuilder.Entity<User>().HasMany(u => u.Uploads).WithOne(u => u.User);
modelBuilder.Entity<User>().HasMany(u => u.Pastes).WithOne(u => u.User);
@ -86,6 +90,9 @@ namespace Teknik.Data
//modelBuilder.Entity<InviteCode>().HasOne(t => t.ClaimedUser).WithOne(u => u.ClaimedInviteCode).HasPrincipalKey("ClaimedUserId").HasForeignKey("ClaimedUser_UserId"); // Legacy???
//modelBuilder.Entity<InviteCode>().HasOne(t => t.Owner).WithMany(u => u.OwnedInviteCodes).HasPrincipalKey("ClaimedUserId").HasForeignKey("Owner_UserId"); // Legacy???
// Billing Customers
modelBuilder.Entity<Customer>().HasOne(u => u.User);
// Features
modelBuilder.Entity<UserFeature>().HasOne(f => f.Feature);
modelBuilder.Entity<UserFeature>().HasOne(f => f.User);
@ -119,6 +126,8 @@ namespace Teknik.Data
//modelBuilder.Entity<UserSettings>().ToTable("Users");
//modelBuilder.Entity<BlogSettings>().ToTable("Users");
//modelBuilder.Entity<UploadSettings>().ToTable("Users");
// Billing Customers
modelBuilder.Entity<Customer>().ToTable("Customers");
// Features
modelBuilder.Entity<Feature>().ToTable("Features");
// Blogs

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,49 @@

$(document).ready(function () {
$('#subscribeModal').on('shown.bs.modal', function () {
$("#subscribeStatus").css('display', 'none', 'important');
$("#subscribeStatus").html('');
});
$("#subscribeSubmit").click(function () {
// Reset register status messages
$("#subscribeStatus").css('display', 'none', 'important');
$("#subscribeStatus").html('');
// Disable the register button
disableButton('#subscribeSubmit', 'Subscribing...');
var form = $('#registrationForm');
$.ajax({
type: "POST",
url: form.attr('action'),
data: form.serialize(),
headers: { 'X-Requested-With': 'XMLHttpRequest' },
xhrFields: {
withCredentials: true
},
success: function (html) {
if (html.success) {
$('#registerModal').modal('hide');
$("#top_msg").css('display', 'inline', 'important');
$("#top_msg").html('<div class="alert alert-success alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>Registration Successful. Redirecting...</div>');
window.location = html.redirectUrl;
}
else {
$("#registerStatus").css('display', 'inline', 'important');
$("#registerStatus").html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' + parseErrorMessage(html) + '</div>');
}
},
error: function (response) {
$("#registerStatus").css('display', 'inline', 'important');
$("#registerStatus").html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' + parseErrorMessage(response.responseText) + '</div>');
}
}).always(function () {
enableButton('#registerSubmit', 'Sign Up');
});
return false;
});
});

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Teknik.Utilities.Cryptography
{
public static class Crypto
{
public static string GenerateKey(int keySize)
{
return StringHelper.RandomString(keySize / 8);
}
public static string GenerateIV(int ivSize)
{
return StringHelper.RandomString(ivSize / 16);
}
public static string HashPassword(string key, string password)
{
return SHA384.Hash(key, password).ToHex();
}
}
}