Merge pull request #227 from Moonlight-Panel/StripeIntegration

Implemented a basic stripe integration
This commit is contained in:
Marcel Baumgartner
2023-07-13 21:36:57 +02:00
committed by GitHub
27 changed files with 1948 additions and 382 deletions

View File

@@ -39,8 +39,6 @@ public class ConfigV1
[JsonProperty("Cleanup")] public CleanupData Cleanup { get; set; } = new(); [JsonProperty("Cleanup")] public CleanupData Cleanup { get; set; } = new();
[JsonProperty("Subscriptions")] public SubscriptionsData Subscriptions { get; set; } = new();
[JsonProperty("DiscordNotifications")] public DiscordNotificationsData DiscordNotifications { get; set; } = new(); [JsonProperty("DiscordNotifications")] public DiscordNotificationsData DiscordNotifications { get; set; } = new();
[JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new(); [JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new();
@@ -50,6 +48,15 @@ public class ConfigV1
[JsonProperty("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new(); [JsonProperty("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new();
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new(); [JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
[JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new();
}
public class StripeData
{
[JsonProperty("ApiKey")]
[Description("Put here your stripe api key if you add subscriptions. Currently the only billing option is stripe which is enabled by default and cannot be turned off. This feature is still experimental")]
public string ApiKey { get; set; } = "";
} }
public class AuthData public class AuthData
@@ -318,11 +325,6 @@ public class ConfigV1
[JsonProperty("Wait")] public long Wait { get; set; } = 15; [JsonProperty("Wait")] public long Wait { get; set; } = 15;
} }
public class SubscriptionsData
{
[JsonProperty("SellPass")] public SellPassData SellPass { get; set; } = new();
}
public class SellPassData public class SellPassData
{ {
[JsonProperty("Enable")] public bool Enable { get; set; } = false; [JsonProperty("Enable")] public bool Enable { get; set; } = false;

View File

@@ -1,9 +1,16 @@
namespace Moonlight.App.Database.Entities; using Moonlight.App.Models.Misc;
namespace Moonlight.App.Database.Entities;
public class Subscription public class Subscription
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Description { get; set; } = ""; public string Description { get; set; } = "";
public Currency Currency { get; set; } = Currency.USD;
public double Price { get; set; }
public string StripeProductId { get; set; } = "";
public string StripePriceId { get; set; } = "";
public string LimitsJson { get; set; } = ""; public string LimitsJson { get; set; } = "";
public int Duration { get; set; } = 30;
} }

View File

@@ -51,8 +51,8 @@ public class User
// Subscriptions // Subscriptions
public Subscription? CurrentSubscription { get; set; } = null; public Subscription? CurrentSubscription { get; set; } = null;
public DateTime SubscriptionSince { get; set; } = DateTime.Now; public DateTime SubscriptionSince { get; set; } = DateTime.UtcNow;
public int SubscriptionDuration { get; set; } public DateTime SubscriptionExpires { get; set; } = DateTime.UtcNow;
// Ip logs // Ip logs
public string RegisterIp { get; set; } = ""; public string RegisterIp { get; set; } = "";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedStripeIntegration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SubscriptionDuration",
table: "Users");
migrationBuilder.AddColumn<DateTime>(
name: "SubscriptionExpires",
table: "Users",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<int>(
name: "Currency",
table: "Subscriptions",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "Duration",
table: "Subscriptions",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<double>(
name: "Price",
table: "Subscriptions",
type: "double",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddColumn<string>(
name: "StripePriceId",
table: "Subscriptions",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "StripeProductId",
table: "Subscriptions",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SubscriptionExpires",
table: "Users");
migrationBuilder.DropColumn(
name: "Currency",
table: "Subscriptions");
migrationBuilder.DropColumn(
name: "Duration",
table: "Subscriptions");
migrationBuilder.DropColumn(
name: "Price",
table: "Subscriptions");
migrationBuilder.DropColumn(
name: "StripePriceId",
table: "Subscriptions");
migrationBuilder.DropColumn(
name: "StripeProductId",
table: "Subscriptions");
migrationBuilder.AddColumn<int>(
name: "SubscriptionDuration",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
}
}
}

View File

@@ -663,10 +663,16 @@ namespace Moonlight.App.Database.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("Currency")
.HasColumnType("int");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<int>("Duration")
.HasColumnType("int");
b.Property<string>("LimitsJson") b.Property<string>("LimitsJson")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
@@ -675,6 +681,17 @@ namespace Moonlight.App.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<double>("Price")
.HasColumnType("double");
b.Property<string>("StripePriceId")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StripeProductId")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Subscriptions"); b.ToTable("Subscriptions");
@@ -802,8 +819,8 @@ namespace Moonlight.App.Database.Migrations
b.Property<bool>("StreamerMode") b.Property<bool>("StreamerMode")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
b.Property<int>("SubscriptionDuration") b.Property<DateTime>("SubscriptionExpires")
.HasColumnType("int"); .HasColumnType("datetime(6)");
b.Property<DateTime>("SubscriptionSince") b.Property<DateTime>("SubscriptionSince")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Services;
using Moonlight.App.Services.Sessions;
using Stripe;
using Stripe.Checkout;
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
[ApiController]
[Route("api/moonlight/billing")]
public class BillingController : Controller
{
private readonly IdentityService IdentityService;
private readonly BillingService BillingService;
public BillingController(
IdentityService identityService,
BillingService billingService)
{
IdentityService = identityService;
BillingService = billingService;
}
[HttpGet("cancel")]
public async Task<ActionResult> Cancel()
{
var user = await IdentityService.Get();
if (user == null)
return Redirect("/login");
return Redirect("/profile/subscriptions/close");
}
[HttpGet("success")]
public async Task<ActionResult> Success()
{
var user = await IdentityService.Get();
if (user == null)
return Redirect("/login");
await BillingService.CompleteCheckout(user);
return Redirect("/profile/subscriptions/close");
}
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Models.Forms; namespace Moonlight.App.Models.Forms;
@@ -10,4 +11,8 @@ public class SubscriptionDataModel
[Required(ErrorMessage = "You need to enter a description")] [Required(ErrorMessage = "You need to enter a description")]
public string Description { get; set; } = ""; public string Description { get; set; } = "";
public double Price { get; set; } = 0;
public Currency Currency { get; set; } = Currency.USD;
public int Duration { get; set; } = 30;
} }

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Models.Misc;
public enum Currency
{
USD = 1,
EUR = 2
}

View File

@@ -35,6 +35,7 @@ public class DiscordNotificationService
Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage); Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage);
Event.On<User>("supportChat.close", this, OnSupportChatClose); Event.On<User>("supportChat.close", this, OnSupportChatClose);
Event.On<User>("user.rating", this, OnUserRated); Event.On<User>("user.rating", this, OnUserRated);
Event.On<User>("billing.completed", this, OnBillingCompleted);
} }
else else
{ {
@@ -42,6 +43,21 @@ public class DiscordNotificationService
} }
} }
private async Task OnBillingCompleted(User user)
{
await SendNotification("", builder =>
{
builder.Color = Color.Red;
builder.Title = "New payment received";
builder.AddField("User", user.Email);
builder.AddField("Firstname", user.FirstName);
builder.AddField("Lastname", user.LastName);
builder.AddField("Amount", user.CurrentSubscription!.Price);
builder.AddField("Currency", user.CurrentSubscription!.Currency);
});
}
private async Task OnUserRated(User user) private async Task OnUserRated(User user)
{ {
await SendNotification("", builder => await SendNotification("", builder =>

View File

@@ -0,0 +1,127 @@
using System.Globalization;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Exceptions;
using Moonlight.App.Repositories;
using Moonlight.App.Services.Mail;
using Moonlight.App.Services.Sessions;
using Stripe.Checkout;
using Subscription = Moonlight.App.Database.Entities.Subscription;
namespace Moonlight.App.Services;
public class BillingService
{
private readonly ConfigService ConfigService;
private readonly SubscriptionService SubscriptionService;
private readonly Repository<Subscription> SubscriptionRepository;
private readonly SessionServerService SessionServerService;
private readonly EventSystem Event;
private readonly MailService MailService;
public BillingService(
ConfigService configService,
SubscriptionService subscriptionService,
Repository<Subscription> subscriptionRepository,
EventSystem eventSystem,
SessionServerService sessionServerService,
MailService mailService)
{
ConfigService = configService;
SubscriptionService = subscriptionService;
SubscriptionRepository = subscriptionRepository;
Event = eventSystem;
SessionServerService = sessionServerService;
MailService = mailService;
}
public async Task<string> StartCheckout(User user, Subscription subscription)
{
var appUrl = ConfigService.Get().Moonlight.AppUrl;
var controllerUrl = appUrl + "/api/moonlight/billing";
var options = new SessionCreateOptions()
{
LineItems = new()
{
new()
{
Price = subscription.StripePriceId,
Quantity = 1
}
},
Mode = "payment",
SuccessUrl = controllerUrl + "/success",
CancelUrl = controllerUrl + "/cancel",
AutomaticTax = new SessionAutomaticTaxOptions()
{
Enabled = true
},
CustomerEmail = user.Email.ToLower(),
Metadata = new()
{
{
"productId",
subscription.StripeProductId
}
}
};
var service = new SessionService();
var session = await service.CreateAsync(options);
return session.Url;
}
public async Task CompleteCheckout(User user)
{
var sessionService = new SessionService();
var sessionsPerUser = await sessionService.ListAsync(new SessionListOptions()
{
CustomerDetails = new()
{
Email = user.Email
}
});
var latestCompletedSession = sessionsPerUser
.Where(x => x.Status == "complete")
.Where(x => x.PaymentStatus == "paid")
.MaxBy(x => x.Created);
if (latestCompletedSession == null)
throw new DisplayException("No completed session found");
var productId = latestCompletedSession.Metadata["productId"];
var subscription = SubscriptionRepository
.Get()
.FirstOrDefault(x => x.StripeProductId == productId);
if (subscription == null)
throw new DisplayException("No subscription for this product found");
// if (await SubscriptionService.GetActiveSubscription(user) != null)
// {
// return;
// }
await SubscriptionService.SetActiveSubscription(user, subscription);
await MailService.SendMail(user, "checkoutComplete", values =>
{
values.Add("SubscriptionName", subscription.Name);
values.Add("SubscriptionPrice", subscription.Price
.ToString(CultureInfo.InvariantCulture));
values.Add("SubscriptionCurrency", subscription.Currency
.ToString());
values.Add("SubscriptionDuration", subscription.Duration
.ToString(CultureInfo.InvariantCulture));
});
await Event.Emit("billing.completed", user);
await SessionServerService.ReloadUserSessions(user);
}
}

View File

@@ -0,0 +1,18 @@
using Microsoft.JSInterop;
namespace Moonlight.App.Services.Interop;
public class PopupService
{
private readonly IJSRuntime JsRuntime;
public PopupService(IJSRuntime jsRuntime)
{
JsRuntime = jsRuntime;
}
public async Task ShowCentered(string url, string title, int width = 500, int height = 500)
{
await JsRuntime.InvokeVoidAsync("moonlight.popup.showCentered", url, title, width, height);
}
}

View File

@@ -1,45 +0,0 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Newtonsoft.Json;
namespace Moonlight.App.Services;
public class SubscriptionAdminService
{
private readonly SubscriptionRepository SubscriptionRepository;
private readonly OneTimeJwtService OneTimeJwtService;
public SubscriptionAdminService(OneTimeJwtService oneTimeJwtService, SubscriptionRepository subscriptionRepository)
{
OneTimeJwtService = oneTimeJwtService;
SubscriptionRepository = subscriptionRepository;
}
public Task<SubscriptionLimit[]> GetLimits(Subscription subscription)
{
return Task.FromResult(
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
?? Array.Empty<SubscriptionLimit>()
);
}
public Task SaveLimits(Subscription subscription, SubscriptionLimit[] limits)
{
subscription.LimitsJson = JsonConvert.SerializeObject(limits);
SubscriptionRepository.Update(subscription);
return Task.CompletedTask;
}
public Task<string> GenerateCode(Subscription subscription, int duration)
{
return Task.FromResult(
OneTimeJwtService.Generate(data =>
{
data.Add("subscription", subscription.Id.ToString());
data.Add("duration", duration.ToString());
}, TimeSpan.FromDays(10324))
);
}
}

View File

@@ -1,151 +1,200 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services.Sessions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Stripe;
using File = System.IO.File;
using Subscription = Moonlight.App.Database.Entities.Subscription;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
public class SubscriptionService public class SubscriptionService
{ {
private readonly SubscriptionRepository SubscriptionRepository; private readonly Repository<Subscription> SubscriptionRepository;
private readonly OneTimeJwtService OneTimeJwtService; private readonly Repository<User> UserRepository;
private readonly IdentityService IdentityService;
private readonly UserRepository UserRepository;
public SubscriptionService( public SubscriptionService(
SubscriptionRepository subscriptionRepository, Repository<Subscription> subscriptionRepository,
OneTimeJwtService oneTimeJwtService, Repository<User> userRepository)
IdentityService identityService,
UserRepository userRepository
)
{ {
SubscriptionRepository = subscriptionRepository; SubscriptionRepository = subscriptionRepository;
OneTimeJwtService = oneTimeJwtService;
IdentityService = identityService;
UserRepository = userRepository; UserRepository = userRepository;
} }
public async Task<Subscription?> GetCurrent() public async Task<Subscription> Create(string name, string description, Currency currency, double price, int duration)
{ {
var user = await GetCurrentUser(); var optionsProduct = new ProductCreateOptions
if (user == null || user.CurrentSubscription == null)
return null;
var subscriptionEnd = user.SubscriptionSince.ToUniversalTime().AddDays(user.SubscriptionDuration);
if (subscriptionEnd > DateTime.UtcNow)
{ {
Name = name,
Description = description,
DefaultPriceData = new()
{
UnitAmount = (long)(price * 100),
Currency = currency.ToString().ToLower()
}
};
var productService = new ProductService();
var product = await productService.CreateAsync(optionsProduct);
var subscription = new Subscription()
{
Name = name,
Description = description,
Currency = currency,
Price = price,
Duration = duration,
LimitsJson = "[]",
StripeProductId = product.Id,
StripePriceId = product.DefaultPriceId
};
return SubscriptionRepository.Add(subscription);
}
public async Task Update(Subscription subscription)
{
// Create the new price object
var optionsPrice = new PriceCreateOptions
{
UnitAmount = (long)(subscription.Price * 100),
Currency = subscription.Currency.ToString().ToLower(),
Product = subscription.StripeProductId
};
var servicePrice = new PriceService();
var price = await servicePrice.CreateAsync(optionsPrice);
// Update the product
var productService = new ProductService();
var product = await productService.UpdateAsync(subscription.StripeProductId, new()
{
Name = subscription.Name,
Description = subscription.Description,
DefaultPrice = price.Id
});
// Disable old price
await servicePrice.UpdateAsync(subscription.StripePriceId, new()
{
Active = false
});
// Update the model
subscription.StripeProductId = product.Id;
subscription.StripePriceId = product.DefaultPriceId;
SubscriptionRepository.Update(subscription);
}
public async Task Delete(Subscription subscription)
{
var productService = new ProductService();
await productService.DeleteAsync(subscription.StripeProductId);
SubscriptionRepository.Delete(subscription);
}
public Task UpdateLimits(Subscription subscription, SubscriptionLimit[] limits)
{
subscription.LimitsJson = JsonConvert.SerializeObject(limits);
SubscriptionRepository.Update(subscription);
return Task.CompletedTask;
}
public Task<SubscriptionLimit[]> GetLimits(Subscription subscription)
{
var limits =
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson) ?? Array.Empty<SubscriptionLimit>();
return Task.FromResult(limits);
}
public async Task<Subscription?> GetActiveSubscription(User u)
{
var user = await EnsureData(u);
if (user.CurrentSubscription != null)
{
if (user.SubscriptionExpires < DateTime.UtcNow)
{
user.CurrentSubscription = null;
UserRepository.Update(user);
}
}
return user.CurrentSubscription; return user.CurrentSubscription;
} }
public async Task CancelSubscription(User u)
return null;
}
public async Task ApplyCode(string code)
{ {
var data = await OneTimeJwtService.Validate(code); var user = await EnsureData(u);
if (data == null)
throw new DisplayException("Invalid or expired subscription code");
var id = int.Parse(data["subscription"]);
var duration = int.Parse(data["duration"]);
var subscription = SubscriptionRepository
.Get()
.FirstOrDefault(x => x.Id == id);
if (subscription == null)
throw new DisplayException("The subscription the code is associated with does not exist");
var user = await GetCurrentUser();
if (user == null)
throw new DisplayException("Unable to determine current user");
user.CurrentSubscription = subscription;
user.SubscriptionDuration = duration;
user.SubscriptionSince = DateTime.UtcNow;
UserRepository.Update(user);
await OneTimeJwtService.Revoke(code);
}
public async Task Cancel()
{
if (await GetCurrent() != null)
{
var user = await GetCurrentUser();
user.CurrentSubscription = null; user.CurrentSubscription = null;
UserRepository.Update(user);
}
public async Task SetActiveSubscription(User u, Subscription subscription)
{
var user = await EnsureData(u);
user.SubscriptionSince = DateTime.UtcNow;
user.SubscriptionExpires = DateTime.UtcNow.AddDays(subscription.Duration);
user.CurrentSubscription = subscription;
UserRepository.Update(user); UserRepository.Update(user);
} }
}
public async Task<SubscriptionLimit> GetLimit(string identifier) // Cache, optimize sql code public async Task<SubscriptionLimit[]> GetDefaultLimits()
{
var subscription = await GetCurrent();
var defaultLimits = await GetDefaultLimits();
if (subscription == null)
{
// If the default subscription limit with identifier is found, return it. if not, return empty
return defaultLimits.FirstOrDefault(x => x.Identifier == identifier) ?? new()
{
Identifier = identifier,
Amount = 0
};
}
var subscriptionLimits =
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
?? Array.Empty<SubscriptionLimit>();
var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier);
if (foundLimit != null)
return foundLimit;
// If the default subscription limit with identifier is found, return it. if not, return empty
return defaultLimits.FirstOrDefault(x => x.Identifier == identifier) ?? new()
{
Identifier = identifier,
Amount = 0
};
}
private async Task<User?> GetCurrentUser()
{
var user = await IdentityService.Get();
if (user == null)
return null;
var userWithData = UserRepository
.Get()
.Include(x => x.CurrentSubscription)
.First(x => x.Id == user.Id);
return userWithData;
}
private async Task<SubscriptionLimit[]> GetDefaultLimits() // Add cache and reload option
{ {
var defaultSubscriptionJson = "[]"; var defaultSubscriptionJson = "[]";
var path = PathBuilder.File("storage", "configs", "default_subscription.json");
if (File.Exists(PathBuilder.File("storage", "configs", "default_subscription.json"))) if (File.Exists(path))
{ {
defaultSubscriptionJson = defaultSubscriptionJson =
await File.ReadAllTextAsync(PathBuilder.File("storage", "configs", "default_subscription.json")); await File.ReadAllTextAsync(path);
} }
return JsonConvert.DeserializeObject<SubscriptionLimit[]>(defaultSubscriptionJson) ?? Array.Empty<SubscriptionLimit>(); return JsonConvert.DeserializeObject<SubscriptionLimit[]>(defaultSubscriptionJson)
?? Array.Empty<SubscriptionLimit>();
}
public async Task<SubscriptionLimit> GetLimit(User u, string identifier)
{
var subscription = await GetActiveSubscription(u);
var defaultLimits = await GetDefaultLimits();
if (subscription != null) // User has a active subscriptions
{
var subscriptionLimits = await GetLimits(subscription);
var subscriptionLimit = subscriptionLimits
.FirstOrDefault(x => x.Identifier == identifier);
if (subscriptionLimit != null) // Found subscription limit for the user's subscription
return subscriptionLimit;
} // If were are here, the user's subscription has no limit for this identifier, so we fallback to default
var defaultSubscriptionLimit = defaultLimits
.FirstOrDefault(x => x.Identifier == identifier);
if (defaultSubscriptionLimit != null)
return defaultSubscriptionLimit; // Default subscription limit found
return new() // No default subscription limit found
{
Identifier = identifier,
Amount = 0
};
}
private Task<User> EnsureData(User u)
{
var user = UserRepository
.Get()
.Include(x => x.CurrentSubscription)
.First(x => x.Id == u.Id);
return Task.FromResult(user);
} }
} }

View File

@@ -53,6 +53,7 @@
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.1-dev-00910" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.1.1-dev-00910" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00947" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00947" />
<PackageReference Include="SSH.NET" Version="2020.0.2" /> <PackageReference Include="SSH.NET" Version="2020.0.2" />
<PackageReference Include="Stripe.net" Version="41.23.0-beta.1" />
<PackageReference Include="UAParser" Version="3.1.47" /> <PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="XtermBlazor" Version="1.8.1" /> <PackageReference Include="XtermBlazor" Version="1.8.1" />
</ItemGroup> </ItemGroup>

View File

@@ -1,6 +1,5 @@
using BlazorDownloadFile; using BlazorDownloadFile;
using BlazorTable; using BlazorTable;
using CurrieTechnologies.Razor.SweetAlert2;
using HealthChecks.UI.Client; using HealthChecks.UI.Client;
using Moonlight.App.ApiClients.CloudPanel; using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.ApiClients.Daemon; using Moonlight.App.ApiClients.Daemon;
@@ -33,6 +32,8 @@ using Moonlight.App.Services.SupportChat;
using Sentry; using Sentry;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
using Stripe;
using SubscriptionService = Moonlight.App.Services.SubscriptionService;
namespace Moonlight namespace Moonlight
{ {
@@ -208,9 +209,9 @@ namespace Moonlight
builder.Services.AddScoped<DynamicBackgroundService>(); builder.Services.AddScoped<DynamicBackgroundService>();
builder.Services.AddScoped<ServerAddonPluginService>(); builder.Services.AddScoped<ServerAddonPluginService>();
builder.Services.AddScoped<KeyListenerService>(); builder.Services.AddScoped<KeyListenerService>();
builder.Services.AddScoped<PopupService>();
builder.Services.AddScoped<SubscriptionService>(); builder.Services.AddScoped<SubscriptionService>();
builder.Services.AddScoped<SubscriptionAdminService>(); builder.Services.AddScoped<BillingService>();
builder.Services.AddScoped<SessionClientService>(); builder.Services.AddScoped<SessionClientService>();
builder.Services.AddSingleton<SessionServerService>(); builder.Services.AddSingleton<SessionServerService>();
@@ -251,6 +252,10 @@ namespace Moonlight
builder.Services.AddBlazorContextMenu(); builder.Services.AddBlazorContextMenu();
builder.Services.AddBlazorDownloadFile(); builder.Services.AddBlazorDownloadFile();
StripeConfiguration.ApiKey = configService
.Get()
.Moonlight.Stripe.ApiKey;
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.

View File

@@ -6,6 +6,7 @@
@using Moonlight.App.ApiClients.Modrinth @using Moonlight.App.ApiClients.Modrinth
@using Moonlight.App.ApiClients.Wings @using Moonlight.App.ApiClients.Wings
@using Moonlight.App.Helpers @using Moonlight.App.Helpers
@using Stripe
@inherits ErrorBoundaryBase @inherits ErrorBoundaryBase
@inject AlertService AlertService @inject AlertService AlertService
@@ -105,6 +106,13 @@ else
{ {
await AlertService.Error(SmartTranslateService.Translate("This function is not implemented")); await AlertService.Error(SmartTranslateService.Translate("This function is not implemented"));
} }
else if (exception is StripeException stripeException)
{
await AlertService.Error(
SmartTranslateService.Translate("Unknown error from stripe"),
stripeException.Message
);
}
else else
{ {
Logger.Warn(exception); Logger.Warn(exception);

View File

@@ -4,10 +4,11 @@
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Mappy.Net
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject SubscriptionRepository SubscriptionRepository @inject SubscriptionRepository SubscriptionRepository
@inject SubscriptionAdminService SubscriptionAdminService @inject SubscriptionService SubscriptionService
<OnlyAdmin> <OnlyAdmin>
<div class="card card-body p-10"> <div class="card card-body p-10">
@@ -31,7 +32,37 @@
<TL>Description</TL> <TL>Description</TL>
</label> </label>
<div class="input-group mb-5"> <div class="input-group mb-5">
<InputText @bind-Value="Model.Description" class="form-control"></InputText> <InputTextArea @bind-Value="Model.Description" class="form-control"></InputTextArea>
</div>
<label class="form-label">
<TL>Price</TL>
</label>
<div class="input-group mb-5">
<InputNumber @bind-Value="Model.Price" class="form-control"></InputNumber>
</div>
<label class="form-label">
<TL>Currency</TL>
</label>
<div class="input-group mb-5">
<select @bind="Model.Currency" class="form-select">
@foreach (var currency in (Currency[])Enum.GetValues(typeof(Currency)))
{
if (Model.Currency == currency)
{
<option value="@(currency)" selected="">@(currency)</option>
}
else
{
<option value="@(currency)">@(currency)</option>
}
}
</select>
</div>
<label class="form-label">
<TL>Duration</TL>
</label>
<div class="input-group mb-5">
<InputNumber @bind-Value="Model.Duration" class="form-control"></InputNumber>
</div> </div>
<div> <div>
@@ -141,12 +172,10 @@
private async Task OnSubmit() private async Task OnSubmit()
{ {
Subscription!.Name = Model.Name; Subscription = Mapper.Map(Subscription, Model);
Subscription.Description = Model.Description; await SubscriptionService.Update(Subscription!);
SubscriptionRepository.Update(Subscription); await SubscriptionService.UpdateLimits(Subscription!, Limits.ToArray());
await SubscriptionAdminService.SaveLimits(Subscription, Limits.ToArray());
NavigationManager.NavigateTo("/admin/subscriptions"); NavigationManager.NavigateTo("/admin/subscriptions");
} }
@@ -159,10 +188,9 @@
if (Subscription != null) if (Subscription != null)
{ {
Model.Name = Subscription.Name; Model = Mapper.Map<SubscriptionDataModel>(Subscription);
Model.Description = Subscription.Description;
Limits = (await SubscriptionAdminService.GetLimits(Subscription)).ToList(); Limits = (await SubscriptionService.GetLimits(Subscription)).ToList();
} }
} }
} }

View File

@@ -7,10 +7,7 @@
@inject SmartTranslateService SmartTranslateService @inject SmartTranslateService SmartTranslateService
@inject SubscriptionRepository SubscriptionRepository @inject SubscriptionRepository SubscriptionRepository
@inject SubscriptionService SubscriptionService
@inject SubscriptionAdminService SubscriptionAdminService
@inject AlertService AlertService
@inject ClipboardService ClipboardService
<OnlyAdmin> <OnlyAdmin>
<div class="card"> <div class="card">
@@ -34,7 +31,10 @@
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/> <Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true"/> <Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true"/>
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Description"))" Field="@(x => x.Description)" Sortable="true" Filterable="true"/> <Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Description"))" Field="@(x => x.Description)" Sortable="true" Filterable="true"/>
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false"> <Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Price"))" Field="@(x => x.Price)" Sortable="true" Filterable="true"/>
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Currency"))" Field="@(x => x.Currency)" Sortable="true" Filterable="true"/>
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Duration"))" Field="@(x => x.Duration)" Sortable="true" Filterable="true"/>
<Column TableItem="Subscription" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template> <Template>
<a href="/admin/subscriptions/edit/@(context.Id)/"> <a href="/admin/subscriptions/edit/@(context.Id)/">
<TL>Manage</TL> <TL>Manage</TL>
@@ -43,14 +43,9 @@
</Column> </Column>
<Column TableItem="Subscription" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false"> <Column TableItem="Subscription" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template> <Template>
<div class="float-end"> <DeleteButton Confirm="true"
<WButton Text="@(SmartTranslateService.Translate("Create code"))" OnClick="() => Delete(context)">
WorkingText="@(SmartTranslateService.Translate("Working"))" </DeleteButton>
CssClasses="btn-primary"
OnClick="() => GenerateCode(context)">
</WButton>
<DeleteButton Confirm="true" OnClick="() => Delete(context)"/>
</div>
</Template> </Template>
</Column> </Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/> <Pager ShowPageNumber="true" ShowTotalCount="true"/>
@@ -77,25 +72,7 @@
private async Task Delete(Subscription subscription) private async Task Delete(Subscription subscription)
{ {
SubscriptionRepository.Delete(subscription); await SubscriptionService.Delete(subscription);
await LazyLoader.Reload(); await LazyLoader.Reload();
} }
private async Task GenerateCode(Subscription subscription)
{
var durationText = await AlertService.Text(
SmartTranslateService.Translate("Duration"),
SmartTranslateService.Translate("Enter duration of subscription"),
"30"
);
if (int.TryParse(durationText, out int duration))
{
var code = await SubscriptionAdminService.GenerateCode(subscription, duration);
await ClipboardService.Copy(code);
await AlertService.Success(SmartTranslateService.Translate("Copied code to clipboard"));
}
}
} }

View File

@@ -6,7 +6,7 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject SubscriptionRepository SubscriptionRepository @inject SubscriptionRepository SubscriptionRepository
@inject SubscriptionAdminService SubscriptionAdminService @inject SubscriptionService SubscriptionService
<OnlyAdmin> <OnlyAdmin>
<div class="card card-body p-10"> <div class="card card-body p-10">
@@ -21,7 +21,37 @@
<TL>Description</TL> <TL>Description</TL>
</label> </label>
<div class="input-group mb-5"> <div class="input-group mb-5">
<InputText @bind-Value="Model.Description" class="form-control"></InputText> <InputTextArea @bind-Value="Model.Description" class="form-control"></InputTextArea>
</div>
<label class="form-label">
<TL>Price</TL>
</label>
<div class="input-group mb-5">
<InputNumber @bind-Value="Model.Price" class="form-control"></InputNumber>
</div>
<label class="form-label">
<TL>Currency</TL>
</label>
<div class="input-group mb-5">
<select @bind="Model.Currency" class="form-select">
@foreach (var currency in (Currency[])Enum.GetValues(typeof(Currency)))
{
if (Model.Currency == currency)
{
<option value="@(currency)" selected="">@(currency)</option>
}
else
{
<option value="@(currency)">@(currency)</option>
}
}
</select>
</div>
<label class="form-label">
<TL>Duration</TL>
</label>
<div class="input-group mb-5">
<InputNumber @bind-Value="Model.Duration" class="form-control"></InputNumber>
</div> </div>
<div> <div>
@@ -124,13 +154,15 @@
private async Task OnSubmit() private async Task OnSubmit()
{ {
var sub = SubscriptionRepository.Add(new() var sub = await SubscriptionService.Create(
{ Model.Name,
Name = Model.Name, Model.Description,
Description = Model.Description Model.Currency,
}); Model.Price,
Model.Duration
);
await SubscriptionAdminService.SaveLimits(sub, Limits.ToArray()); await SubscriptionService.UpdateLimits(sub, Limits.ToArray());
NavigationManager.NavigateTo("/admin/subscriptions"); NavigationManager.NavigateTo("/admin/subscriptions");
} }

View File

@@ -130,12 +130,12 @@
Model = new(); Model = new();
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription")); await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
Subscription = await SubscriptionService.GetCurrent(); Subscription = await SubscriptionService.GetActiveSubscription(User);
AllowOrder = DomainRepository AllowOrder = DomainRepository
.Get() .Get()
.Include(x => x.Owner) .Include(x => x.Owner)
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("domains")).Amount; .Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit(User, "domains")).Amount;
await lazyLoader.SetText("Loading shared domains"); await lazyLoader.SetText("Loading shared domains");
SharedDomains = SharedDomainRepository.Get().ToArray(); SharedDomains = SharedDomainRepository.Get().ToArray();
@@ -146,7 +146,7 @@
if (DomainRepository if (DomainRepository
.Get() .Get()
.Include(x => x.Owner) .Include(x => x.Owner)
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("domains")).Amount) .Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit(User, "domains")).Amount)
{ {
var domain = await DomainService.Create(Model.Name, Model.SharedDomain, User); var domain = await DomainService.Create(Model.Name, Model.SharedDomain, User);

View File

@@ -1,111 +0,0 @@
@page "/profile/subscriptions"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Services.Interop
@inject ConfigService ConfigService
@inject AlertService AlertService
@inject SubscriptionService SubscriptionService
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="3"/>
<div class="card mb-3">
<div class="row g-0">
<div class="col-md-4 p-10">
<img src="/assets/media/svg/subscription.svg" class="img-fluid rounded-start" alt="Subscription">
</div>
<div class="col-md-8">
<div class="card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
@if (Subscription == null)
{
var config = ConfigService
.Get()
.Moonlight.Subscriptions.SellPass;
var enableSellpass = config.Enable;
var url = config.Url;
<h3 class="mb-2">
<div class="input-group mb-3">
<input @bind="Code" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Enter code"))">
<WButton Text="@(SmartTranslateService.Translate("Submit"))"
WorkingText="@(SmartTranslateService.Translate("Working"))"
CssClasses="btn btn-primary"
OnClick="OnSubmit">
</WButton>
</div>
</h3>
if (enableSellpass)
{
<div class="d-flex justify-content-end pb-0 px-0">
<a href="@(url)" class="btn btn-light">Buy subscription</a>
</div>
}
}
else
{
var d = User.SubscriptionSince.AddDays(User.SubscriptionDuration).ToUniversalTime();
<h3 class="mb-2">
<TL>Active until</TL> @(Formatter.FormatDateOnly(d))
</h3>
<p class="fs-5 text-gray-600 fw-semibold">
<TL>Current subscription</TL>: @(Subscription.Name)
</p>
<p class="fs-6 text-gray-600 fw-semibold">
@(Subscription.Description)
</p>
<p class="fs-7 text-gray-600 fw-semibold">
<TL>We will send you a notification upon subscription expiration</TL>
</p>
<div class="d-flex justify-content-end pb-0 px-0">
<WButton Text="@(SmartTranslateService.Translate("Cancel"))"
WorkingText="@(SmartTranslateService.Translate("Working"))"
CssClasses="btn btn-light"
OnClick="Cancel">
</WButton>
</div>
}
</LazyLoader>
</div>
</div>
</div>
</div>
@code
{
[CascadingParameter]
public User User { get; set; }
private Subscription? Subscription;
private LazyLoader LazyLoader;
private string Code = "";
private async Task Load(LazyLoader arg)
{
Subscription = await SubscriptionService.GetCurrent();
}
private async Task Cancel()
{
if (await AlertService.ConfirmMath())
{
await SubscriptionService.Cancel();
await LazyLoader.Reload();
}
}
private async Task OnSubmit()
{
await SubscriptionService.ApplyCode(Code);
Code = "";
await LazyLoader.Reload();
}
}

View File

@@ -0,0 +1,5 @@
@page "/profile/subscriptions/close"
<script suppress-error="BL9992">
window.close();
</script>

View File

@@ -0,0 +1,170 @@
@page "/profile/subscriptions"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories
@using Moonlight.App.Services.Interop
@using Markdig
@inject BillingService BillingService
@inject Repository<Subscription> SubscriptionRepository
@inject AlertService AlertService
@inject PopupService PopupService
@inject SubscriptionService SubscriptionService
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="3"/>
<LazyLoader @ref="LazyLoader" Load="Load">
@if (CurrentSubscription == null)
{
<div class="card">
<div class="card-body p-lg-17">
<div class="d-flex flex-column">
<div class="mb-13 text-center">
<h1 class="fs-1 fw-bold mb-2">
<TL>Chose your plan</TL>
</h1>
<div class="text-gray-400 fw-semibold fs-3">
<TL>Select the perfect plan for your next project</TL>
</div>
</div>
<div class="row g-10">
@foreach (var subscription in Subscriptions)
{
<div class="col-xl-4">
<div class="card bg-secondary border border-primary">
<div class="card-header">
<div class="card-title">@(subscription.Name)</div>
</div>
<div class="card-body">
<div class="text-center">
<span class="mb-2 text-primary">@(subscription.Currency)</span>
<span class="fs-3x fw-bold text-primary">
@(subscription.Price)
</span>
<span class="fs-7 fw-semibold opacity-50">
/
<span>
@(subscription.Duration) <TL>Days</TL>
</span>
</span>
</div>
<div class="mt-3 card-text text-center">
@{
var html = (MarkupString)Markdown.ToHtml(subscription.Description);
}
@(html)
</div>
</div>
<div class="card-footer">
<div class="text-center">
<WButton Text="@(SmartTranslateService.Translate("Select"))"
CssClasses="btn btn-primary"
OnClick="() => StartCheckout(subscription)">
</WButton>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
}
else
{
<div class="card">
<div class="card-body">
@if ((User.SubscriptionExpires - DateTime.UtcNow).TotalDays < 5)
{
<div class="notice d-flex bg-light-warning rounded border-warning border mb-12 p-6">
<div class="d-flex flex-stack flex-grow-1 ">
<div class="fw-semibold">
<h4 class="text-gray-900 fw-bold">
<TL>We need your attention!</TL>
</h4>
<div class="fs-6 text-gray-700 ">
<TL>Your subscription expires soon</TL>
<span>
@(Math.Round((User.SubscriptionExpires - DateTime.UtcNow).TotalDays)) <TL>left</TL>
</span>
</div>
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-7">
<h3 class="mb-2">
<TL>Active until</TL> @(Formatter.FormatDateOnly(User.SubscriptionExpires))
</h3>
<p class="fs-6 text-gray-600 fw-semibold mb-6 mb-lg-15">
<TL>We will send you a email upon subscription expiration</TL>
</p>
<div class="fs-5 mb-2">
<span class="text-gray-800 fw-bold me-1">@(CurrentSubscription.Currency) @(CurrentSubscription.Price)</span>
<span class="text-gray-600 fw-semibold">
<TL>per</TL> @(CurrentSubscription.Duration) <TL>days</TL>
</span>
</div>
<div class="fs-6 text-gray-600 fw-semibold">
@(CurrentSubscription.Description)
</div>
</div>
<div class="col-lg-5">
</div>
</div>
<div class="row">
<div class="text-end">
<WButton Text="@(SmartTranslateService.Translate("Cancel"))"
CssClasses="btn btn-danger"
OnClick="Cancel">
</WButton>
</div>
</div>
</div>
</div>
}
</LazyLoader>
@code
{
[CascadingParameter]
public User User { get; set; }
private Subscription[] Subscriptions;
private Subscription? CurrentSubscription;
private LazyLoader LazyLoader;
private async Task Load(LazyLoader lazyLoader)
{
Subscriptions = SubscriptionRepository.Get().ToArray();
CurrentSubscription = await SubscriptionService.GetActiveSubscription(User);
}
private async Task Cancel()
{
if (await AlertService.ConfirmMath())
{
await SubscriptionService.CancelSubscription(User);
await LazyLoader.Reload();
}
}
private async Task StartCheckout(Subscription subscription)
{
var url = await BillingService.StartCheckout(User, subscription);
await PopupService.ShowCentered(
url,
"Moonlight Checkout",
500,
700
);
}
}

View File

@@ -190,7 +190,7 @@
Model = new(); Model = new();
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription")); await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
Subscription = await SubscriptionService.GetCurrent(); Subscription = await SubscriptionService.GetActiveSubscription(User);
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy node")); await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy node"));
DeployNode = await SmartDeployService.GetNode(); DeployNode = await SmartDeployService.GetNode();
@@ -200,7 +200,7 @@
foreach (var image in images) foreach (var image in images)
{ {
var limit = await SubscriptionService.GetLimit("image." + image.Id); var limit = await SubscriptionService.GetLimit(User, "image." + image.Id);
if (limit.Amount > 0) if (limit.Amount > 0)
{ {
@@ -219,7 +219,7 @@
private async Task OnValidSubmit() private async Task OnValidSubmit()
{ {
var limit = await SubscriptionService.GetLimit("image." + Model.Image.Id); var limit = await SubscriptionService.GetLimit(User, "image." + Model.Image.Id);
if (limit.Amount > 0) if (limit.Amount > 0)
{ {

View File

@@ -125,7 +125,7 @@
Model = new(); Model = new();
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription")); await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
Subscription = await SubscriptionService.GetCurrent(); Subscription = await SubscriptionService.GetActiveSubscription(User);
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy web host")); await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy web host"));
CloudPanel = await SmartDeployService.GetCloudPanel(); CloudPanel = await SmartDeployService.GetCloudPanel();
@@ -133,7 +133,7 @@
AllowOrder = WebSpaceRepository AllowOrder = WebSpaceRepository
.Get() .Get()
.Include(x => x.Owner) .Include(x => x.Owner)
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("websites")).Amount; .Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit(User, "websites")).Amount;
} }
private async Task OnValidSubmit() private async Task OnValidSubmit()
@@ -141,7 +141,7 @@
if (WebSpaceRepository if (WebSpaceRepository
.Get() .Get()
.Include(x => x.Owner) .Include(x => x.Owner)
.Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit("websites")).Amount) .Count(x => x.Owner.Id == User.Id) < (await SubscriptionService.GetLimit(User, "websites")).Amount)
{ {
var webSpace = await WebSpaceService.Create(Model.BaseDomain, User, CloudPanel); var webSpace = await WebSpaceService.Create(Model.BaseDomain, User, CloudPanel);

View File

@@ -283,37 +283,32 @@
} }
}, },
utils: { utils: {
scrollToElement: function (id) scrollToElement: function (id) {
{
let e = document.getElementById(id); let e = document.getElementById(id);
e.scrollTop = e.scrollHeight; e.scrollTop = e.scrollHeight;
}, },
triggerResizeEvent: function () triggerResizeEvent: function () {
{
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
}, },
showNotification: function (title, text, img) { showNotification: function (title, text, img) {
let notification = new Notification(title, { body: text, icon: img }); let notification = new Notification(title, {body: text, icon: img});
} }
}, },
loading: { loading: {
registerXterm: function() registerXterm: function () {
{
console.log("Registering xterm addons"); console.log("Registering xterm addons");
window.XtermBlazor.registerAddon("xterm-addon-fit", new window.FitAddon.FitAddon()); window.XtermBlazor.registerAddon("xterm-addon-fit", new window.FitAddon.FitAddon());
//window.XtermBlazor.registerAddon("xterm-addon-search", new window.SearchAddon.SearchAddon()); //window.XtermBlazor.registerAddon("xterm-addon-search", new window.SearchAddon.SearchAddon());
//window.XtermBlazor.registerAddon("xterm-addon-web-links", new window.WebLinksAddon.WebLinksAddon()); //window.XtermBlazor.registerAddon("xterm-addon-web-links", new window.WebLinksAddon.WebLinksAddon());
}, },
loadMonaco: function () loadMonaco: function () {
{
console.log("Loading monaco"); console.log("Loading monaco");
monaco.editor.defineTheme('moonlight-theme', { monaco.editor.defineTheme('moonlight-theme', {
base: 'vs-dark', base: 'vs-dark',
inherit: true, inherit: true,
rules: [ rules: [],
],
colors: { colors: {
'editor.background': '#000000' 'editor.background': '#000000'
} }
@@ -321,22 +316,20 @@
} }
}, },
flashbang: { flashbang: {
run: function() run: function () {
{
const light = document.getElementById("flashbang"); const light = document.getElementById("flashbang");
light.style.boxShadow = "0 0 10000px 10000px white, 0 0 250px 10px #FFFFFF"; light.style.boxShadow = "0 0 10000px 10000px white, 0 0 250px 10px #FFFFFF";
light.style.animation = "flashbang 5s linear forwards"; light.style.animation = "flashbang 5s linear forwards";
light.onanimationend = moonlight.flashbang.clean; light.onanimationend = moonlight.flashbang.clean;
}, },
clean: function() clean: function () {
{
const light = document.getElementById("flashbang"); const light = document.getElementById("flashbang");
light.style.animation = ""; light.style.animation = "";
light.style.opacity = "0"; light.style.opacity = "0";
} }
}, },
downloads:{ downloads: {
downloadStream: async function (fileName, contentStreamReference){ downloadStream: async function (fileName, contentStreamReference) {
const arrayBuffer = await contentStreamReference.arrayBuffer(); const arrayBuffer = await contentStreamReference.arrayBuffer();
const blob = new Blob([arrayBuffer]); const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -349,14 +342,11 @@
} }
}, },
keyListener: { keyListener: {
register: function (dotNetObjRef) register: function (dotNetObjRef) {
{ moonlight.keyListener.listener = (event) => {
moonlight.keyListener.listener = (event) =>
{
// filter here what key events should be sent to moonlight // filter here what key events should be sent to moonlight
if(event.code === "KeyS" && event.ctrlKey) if (event.code === "KeyS" && event.ctrlKey) {
{
event.preventDefault(); event.preventDefault();
dotNetObjRef.invokeMethodAsync('OnKeyPress', "saveShortcut"); dotNetObjRef.invokeMethodAsync('OnKeyPress', "saveShortcut");
} }
@@ -364,23 +354,19 @@
window.addEventListener('keydown', moonlight.keyListener.listener); window.addEventListener('keydown', moonlight.keyListener.listener);
}, },
unregister: function (dotNetObjRef) unregister: function (dotNetObjRef) {
{
window.removeEventListener('keydown', moonlight.keyListener.listener); window.removeEventListener('keydown', moonlight.keyListener.listener);
} }
}, },
serverList: { serverList: {
init: function () init: function () {
{ if (moonlight.serverList.Swappable) {
if(moonlight.serverList.Swappable)
{
moonlight.serverList.Swappable.destroy(); moonlight.serverList.Swappable.destroy();
} }
let containers = document.querySelectorAll(".draggable-zone"); let containers = document.querySelectorAll(".draggable-zone");
if (containers.length !== 0) if (containers.length !== 0) {
{
moonlight.serverList.Swappable = new Draggable.Sortable(containers, { moonlight.serverList.Swappable = new Draggable.Sortable(containers, {
draggable: ".draggable", draggable: ".draggable",
handle: ".draggable .draggable-handle", handle: ".draggable .draggable-handle",
@@ -392,8 +378,7 @@
}); });
} }
}, },
getData: function () getData: function () {
{
let groups = new Array(); let groups = new Array();
let groupElements = document.querySelectorAll('[ml-server-group]'); let groupElements = document.querySelectorAll('[ml-server-group]');
@@ -417,5 +402,20 @@
return groups; return groups;
} }
},
popup: {
showCentered: function (url, title, w, h) {
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX;
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY;
const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
const systemZoom = width / window.screen.availWidth;
const left = (width - w) / 2 / systemZoom + dualScreenLeft
const top = (height - h) / 2 / systemZoom + dualScreenTop
const newWindow = window.open(url, title,`scrollbars=yes,width=${w / systemZoom},height=${h / systemZoom},top=${top},left=${left}`)
if (window.focus) newWindow.focus();
}
} }
}; };