From c0df8ac5070841d0f3802766e708685863fe9b72 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Thu, 6 Jul 2023 02:12:06 +0200 Subject: [PATCH 01/54] Implemented new subscription system and basic stripe support --- Moonlight/App/Configuration/ConfigV1.cs | 7 + .../App/Database/Entities/Subscription.cs | 9 +- Moonlight/App/Database/Entities/User.cs | 4 +- ...5171914_AddedStripeIntegration.Designer.cs | 1105 +++++++++++++++++ .../20230705171914_AddedStripeIntegration.cs | 96 ++ .../Migrations/DataContextModelSnapshot.cs | 21 +- .../Api/Moonlight/BillingController.cs | 36 + .../App/Models/Forms/SubscriptionDataModel.cs | 5 + Moonlight/App/Models/Misc/Currency.cs | 7 + Moonlight/App/Services/BillingService.cs | 108 ++ .../App/Services/Interop/PopupService.cs | 18 + .../App/Services/SubscriptionAdminService.cs | 45 - Moonlight/App/Services/SubscriptionService.cs | 243 ++-- Moonlight/Moonlight.csproj | 1 + Moonlight/Program.cs | 11 +- .../ErrorBoundaries/SoftErrorBoundary.razor | 8 + .../Views/Admin/Subscriptions/Edit.razor | 50 +- .../Views/Admin/Subscriptions/Index.razor | 41 +- .../Views/Admin/Subscriptions/New.razor | 50 +- Moonlight/Shared/Views/Domains/Create.razor | 6 +- .../Shared/Views/Profile/Subscriptions.razor | 111 -- .../Views/Profile/Subscriptions/Close.razor | 5 + .../Views/Profile/Subscriptions/Index.razor | 170 +++ Moonlight/Shared/Views/Servers/Create.razor | 6 +- Moonlight/Shared/Views/Webspaces/Create.razor | 6 +- Moonlight/wwwroot/assets/js/moonlight.js | 104 +- 26 files changed, 1899 insertions(+), 374 deletions(-) create mode 100644 Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.Designer.cs create mode 100644 Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.cs create mode 100644 Moonlight/App/Http/Controllers/Api/Moonlight/BillingController.cs create mode 100644 Moonlight/App/Models/Misc/Currency.cs create mode 100644 Moonlight/App/Services/BillingService.cs create mode 100644 Moonlight/App/Services/Interop/PopupService.cs delete mode 100644 Moonlight/App/Services/SubscriptionAdminService.cs delete mode 100644 Moonlight/Shared/Views/Profile/Subscriptions.razor create mode 100644 Moonlight/Shared/Views/Profile/Subscriptions/Close.razor create mode 100644 Moonlight/Shared/Views/Profile/Subscriptions/Index.razor diff --git a/Moonlight/App/Configuration/ConfigV1.cs b/Moonlight/App/Configuration/ConfigV1.cs index 67224fd2..529d2782 100644 --- a/Moonlight/App/Configuration/ConfigV1.cs +++ b/Moonlight/App/Configuration/ConfigV1.cs @@ -43,6 +43,13 @@ public class ConfigV1 [JsonProperty("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new(); [JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new(); + + [JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new(); + } + + public class StripeData + { + [JsonProperty("ApiKey")] public string ApiKey { get; set; } = ""; } public class CleanupData diff --git a/Moonlight/App/Database/Entities/Subscription.cs b/Moonlight/App/Database/Entities/Subscription.cs index ab0b8c77..5e1fd059 100644 --- a/Moonlight/App/Database/Entities/Subscription.cs +++ b/Moonlight/App/Database/Entities/Subscription.cs @@ -1,9 +1,16 @@ -namespace Moonlight.App.Database.Entities; +using Moonlight.App.Models.Misc; + +namespace Moonlight.App.Database.Entities; public class Subscription { public int Id { get; set; } public string Name { 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 int Duration { get; set; } = 30; } \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/User.cs b/Moonlight/App/Database/Entities/User.cs index 1ed0b5c8..5135b3f7 100644 --- a/Moonlight/App/Database/Entities/User.cs +++ b/Moonlight/App/Database/Entities/User.cs @@ -51,8 +51,8 @@ public class User // Subscriptions public Subscription? CurrentSubscription { get; set; } = null; - public DateTime SubscriptionSince { get; set; } = DateTime.Now; - public int SubscriptionDuration { get; set; } + public DateTime SubscriptionSince { get; set; } = DateTime.UtcNow; + public DateTime SubscriptionExpires { get; set; } = DateTime.UtcNow; // Ip logs public string RegisterIp { get; set; } = ""; diff --git a/Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.Designer.cs b/Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.Designer.cs new file mode 100644 index 00000000..18ebe32a --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.Designer.cs @@ -0,0 +1,1105 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.App.Database; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20230705171914_AddedStripeIntegration")] + partial class AddedStripeIntegration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ApiKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ApiUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Host") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("CloudPanels"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("bigint"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Ongoing") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.ToTable("DdosAttacks"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("DockerImages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("SharedDomainId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("SharedDomainId"); + + b.ToTable("Domains"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Allocations") + .HasColumnType("int"); + + b.Property("BackgroundImageUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ConfigFiles") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallEntrypoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Startup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartupDetection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TagsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("LoadingMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.AuditLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("AuditLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.ErrorLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Class") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Stacktrace") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("ErrorLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.SecurityLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SecurityLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WebSpaceId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WebSpaceId"); + + b.ToTable("Databases"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NewsEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Markdown") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("NewsEntries"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HttpPort") + .HasColumnType("int"); + + b.Property("MoonlightDaemonPort") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SftpPort") + .HasColumnType("int"); + + b.Property("Ssl") + .HasColumnType("tinyint(1)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TokenId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServerId"); + + b.ToTable("NodeAllocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NotificationClientId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NotificationClientId"); + + b.ToTable("NotificationActions"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationClients"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Identifier") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Revokes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ArchiveId") + .HasColumnType("int"); + + b.Property("Cpu") + .HasColumnType("int"); + + b.Property("Disk") + .HasColumnType("bigint"); + + b.Property("DockerImageIndex") + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Installing") + .HasColumnType("tinyint(1)"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("IsCleanupException") + .HasColumnType("tinyint(1)"); + + b.Property("MainAllocationId") + .HasColumnType("int"); + + b.Property("Memory") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("OverrideStartup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Suspended") + .HasColumnType("tinyint(1)"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ArchiveId"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Bytes") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerBackups"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SharedDomain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CloudflareId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("SharedDomains"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.StatisticsData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Chart") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("Statistics"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Duration") + .HasColumnType("int"); + + b.Property("LimitsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Price") + .HasColumnType("double"); + + b.Property("StripePriceId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StripeProductId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Attachment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("QuestionType") + .HasColumnType("int"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("SupportChatMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Admin") + .HasColumnType("tinyint(1)"); + + b.Property("City") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Country") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentSubscriptionId") + .HasColumnType("int"); + + b.Property("DiscordId") + .HasColumnType("bigint unsigned"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HasRated") + .HasColumnType("tinyint(1)"); + + b.Property("LastIp") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastVisitedAt") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("RegisterIp") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerListLayoutJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("State") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StreamerMode") + .HasColumnType("tinyint(1)"); + + b.Property("SubscriptionExpires") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionSince") + .HasColumnType("datetime(6)"); + + b.Property("SupportPending") + .HasColumnType("tinyint(1)"); + + b.Property("TokenValidTime") + .HasColumnType("datetime(6)"); + + b.Property("TotpEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("TotpSecret") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurrentSubscriptionId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CloudPanelId") + .HasColumnType("int"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VHostTemplate") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CloudPanelId"); + + b.HasIndex("OwnerId"); + + b.ToTable("WebSpaces"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => + { + b.HasOne("Moonlight.App.Database.Entities.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Node"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("DockerImages") + .HasForeignKey("ImageId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.SharedDomain", "SharedDomain") + .WithMany() + .HasForeignKey("SharedDomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("SharedDomain"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("Variables") + .HasForeignKey("ImageId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => + { + b.HasOne("Moonlight.App.Database.Entities.WebSpace", "WebSpace") + .WithMany("Databases") + .HasForeignKey("WebSpaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WebSpace"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.HasOne("Moonlight.App.Database.Entities.Node", null) + .WithMany("Allocations") + .HasForeignKey("NodeId"); + + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b => + { + b.HasOne("Moonlight.App.Database.Entities.Notification.NotificationClient", "NotificationClient") + .WithMany() + .HasForeignKey("NotificationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationClient"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.HasOne("Moonlight.App.Database.Entities.ServerBackup", "Archive") + .WithMany() + .HasForeignKey("ArchiveId"); + + b.HasOne("Moonlight.App.Database.Entities.Image", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.NodeAllocation", "MainAllocation") + .WithMany() + .HasForeignKey("MainAllocationId"); + + b.HasOne("Moonlight.App.Database.Entities.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Node"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Backups") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.Navigation("Recipient"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription") + .WithMany() + .HasForeignKey("CurrentSubscriptionId"); + + b.Navigation("CurrentSubscription"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.HasOne("Moonlight.App.Database.Entities.CloudPanel", "CloudPanel") + .WithMany() + .HasForeignKey("CloudPanelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CloudPanel"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Backups"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.Navigation("Databases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.cs b/Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.cs new file mode 100644 index 00000000..ec21c0ce --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230705171914_AddedStripeIntegration.cs @@ -0,0 +1,96 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddedStripeIntegration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SubscriptionDuration", + table: "Users"); + + migrationBuilder.AddColumn( + name: "SubscriptionExpires", + table: "Users", + type: "datetime(6)", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "Currency", + table: "Subscriptions", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Duration", + table: "Subscriptions", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Price", + table: "Subscriptions", + type: "double", + nullable: false, + defaultValue: 0.0); + + migrationBuilder.AddColumn( + name: "StripePriceId", + table: "Subscriptions", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "StripeProductId", + table: "Subscriptions", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + 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( + name: "SubscriptionDuration", + table: "Users", + type: "int", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index dce16c4f..701c21d8 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -663,10 +663,16 @@ namespace Moonlight.App.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + b.Property("Currency") + .HasColumnType("int"); + b.Property("Description") .IsRequired() .HasColumnType("longtext"); + b.Property("Duration") + .HasColumnType("int"); + b.Property("LimitsJson") .IsRequired() .HasColumnType("longtext"); @@ -675,6 +681,17 @@ namespace Moonlight.App.Database.Migrations .IsRequired() .HasColumnType("longtext"); + b.Property("Price") + .HasColumnType("double"); + + b.Property("StripePriceId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StripeProductId") + .IsRequired() + .HasColumnType("longtext"); + b.HasKey("Id"); b.ToTable("Subscriptions"); @@ -802,8 +819,8 @@ namespace Moonlight.App.Database.Migrations b.Property("StreamerMode") .HasColumnType("tinyint(1)"); - b.Property("SubscriptionDuration") - .HasColumnType("int"); + b.Property("SubscriptionExpires") + .HasColumnType("datetime(6)"); b.Property("SubscriptionSince") .HasColumnType("datetime(6)"); diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/BillingController.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/BillingController.cs new file mode 100644 index 00000000..0c9990c5 --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/BillingController.cs @@ -0,0 +1,36 @@ +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("success")] + public async Task Success() + { + var user = await IdentityService.Get(); + + if (user == null) + return Redirect("/login"); + + await BillingService.CompleteCheckout(user); + + return Redirect("/profile/subscriptions/close"); + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/SubscriptionDataModel.cs b/Moonlight/App/Models/Forms/SubscriptionDataModel.cs index 92bf8ebc..2f7c7185 100644 --- a/Moonlight/App/Models/Forms/SubscriptionDataModel.cs +++ b/Moonlight/App/Models/Forms/SubscriptionDataModel.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Moonlight.App.Models.Misc; namespace Moonlight.App.Models.Forms; @@ -10,4 +11,8 @@ public class SubscriptionDataModel [Required(ErrorMessage = "You need to enter a description")] public string Description { get; set; } = ""; + + public double Price { get; set; } = 0; + public Currency Currency { get; set; } = Currency.USD; + public int Duration { get; set; } = 30; } \ No newline at end of file diff --git a/Moonlight/App/Models/Misc/Currency.cs b/Moonlight/App/Models/Misc/Currency.cs new file mode 100644 index 00000000..195ae285 --- /dev/null +++ b/Moonlight/App/Models/Misc/Currency.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Models.Misc; + +public enum Currency +{ + USD = 1, + EUR = 2 +} \ No newline at end of file diff --git a/Moonlight/App/Services/BillingService.cs b/Moonlight/App/Services/BillingService.cs new file mode 100644 index 00000000..6ef407f5 --- /dev/null +++ b/Moonlight/App/Services/BillingService.cs @@ -0,0 +1,108 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Events; +using Moonlight.App.Exceptions; +using Moonlight.App.Repositories; +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 SubscriptionRepository; + private readonly SessionServerService SessionServerService; + private readonly EventSystem Event; + + public BillingService( + ConfigService configService, + SubscriptionService subscriptionService, + Repository subscriptionRepository, + EventSystem eventSystem, + SessionServerService sessionServerService) + { + ConfigService = configService; + SubscriptionService = subscriptionService; + SubscriptionRepository = subscriptionRepository; + Event = eventSystem; + SessionServerService = sessionServerService; + } + + public async Task 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 SessionServerService.ReloadUserSessions(user); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Interop/PopupService.cs b/Moonlight/App/Services/Interop/PopupService.cs new file mode 100644 index 00000000..7106e5da --- /dev/null +++ b/Moonlight/App/Services/Interop/PopupService.cs @@ -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); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/SubscriptionAdminService.cs b/Moonlight/App/Services/SubscriptionAdminService.cs deleted file mode 100644 index fbbe84bd..00000000 --- a/Moonlight/App/Services/SubscriptionAdminService.cs +++ /dev/null @@ -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 GetLimits(Subscription subscription) - { - return Task.FromResult( - JsonConvert.DeserializeObject(subscription.LimitsJson) - ?? Array.Empty() - ); - } - - public Task SaveLimits(Subscription subscription, SubscriptionLimit[] limits) - { - subscription.LimitsJson = JsonConvert.SerializeObject(limits); - SubscriptionRepository.Update(subscription); - - return Task.CompletedTask; - } - - public Task 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)) - ); - } -} \ No newline at end of file diff --git a/Moonlight/App/Services/SubscriptionService.cs b/Moonlight/App/Services/SubscriptionService.cs index 2dcc797c..4da9138f 100644 --- a/Moonlight/App/Services/SubscriptionService.cs +++ b/Moonlight/App/Services/SubscriptionService.cs @@ -1,151 +1,200 @@ using Microsoft.EntityFrameworkCore; using Moonlight.App.Database.Entities; -using Moonlight.App.Exceptions; using Moonlight.App.Helpers; using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; -using Moonlight.App.Services.Sessions; using Newtonsoft.Json; +using Stripe; +using File = System.IO.File; +using Subscription = Moonlight.App.Database.Entities.Subscription; namespace Moonlight.App.Services; public class SubscriptionService { - private readonly SubscriptionRepository SubscriptionRepository; - private readonly OneTimeJwtService OneTimeJwtService; - private readonly IdentityService IdentityService; - private readonly UserRepository UserRepository; + private readonly Repository SubscriptionRepository; + private readonly Repository UserRepository; public SubscriptionService( - SubscriptionRepository subscriptionRepository, - OneTimeJwtService oneTimeJwtService, - IdentityService identityService, - UserRepository userRepository - ) + Repository subscriptionRepository, + Repository userRepository) { SubscriptionRepository = subscriptionRepository; - OneTimeJwtService = oneTimeJwtService; - IdentityService = identityService; UserRepository = userRepository; } - public async Task GetCurrent() + public async Task Create(string name, string description, Currency currency, double price, int duration) { - var user = await GetCurrentUser(); - - if (user == null || user.CurrentSubscription == null) - return null; - - var subscriptionEnd = user.SubscriptionSince.ToUniversalTime().AddDays(user.SubscriptionDuration); - - if (subscriptionEnd > DateTime.UtcNow) + var optionsProduct = new ProductCreateOptions { - return user.CurrentSubscription; - } + Name = name, + Description = description, + DefaultPriceData = new() + { + UnitAmount = (long)(price * 100), + Currency = currency.ToString().ToLower() + } + }; - return null; + 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 async Task ApplyCode(string code) + public Task UpdateLimits(Subscription subscription, SubscriptionLimit[] limits) { - var data = await OneTimeJwtService.Validate(code); + subscription.LimitsJson = JsonConvert.SerializeObject(limits); + SubscriptionRepository.Update(subscription); + + return Task.CompletedTask; + } + public Task GetLimits(Subscription subscription) + { + var limits = + JsonConvert.DeserializeObject(subscription.LimitsJson) ?? Array.Empty(); + return Task.FromResult(limits); + } - if (data == null) - throw new DisplayException("Invalid or expired subscription code"); + public async Task GetActiveSubscription(User u) + { + var user = await EnsureData(u); - var id = int.Parse(data["subscription"]); - var duration = int.Parse(data["duration"]); + if (user.CurrentSubscription != null) + { + if (user.SubscriptionExpires < DateTime.UtcNow) + { + user.CurrentSubscription = null; + UserRepository.Update(user); + } + } + + return user.CurrentSubscription; + } + public async Task CancelSubscription(User u) + { + var user = await EnsureData(u); - 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.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); - - await OneTimeJwtService.Revoke(code); } - - public async Task Cancel() + + public async Task GetDefaultLimits() { - if (await GetCurrent() != null) + var defaultSubscriptionJson = "[]"; + var path = PathBuilder.File("storage", "configs", "default_subscription.json"); + + if (File.Exists(path)) { - var user = await GetCurrentUser(); - - user.CurrentSubscription = null; - - UserRepository.Update(user); + defaultSubscriptionJson = + await File.ReadAllTextAsync(path); } - } - public async Task GetLimit(string identifier) // Cache, optimize sql code + return JsonConvert.DeserializeObject(defaultSubscriptionJson) + ?? Array.Empty(); + } + public async Task GetLimit(User u, string identifier) { - var subscription = await GetCurrent(); + var subscription = await GetActiveSubscription(u); var defaultLimits = await GetDefaultLimits(); - if (subscription == null) + if (subscription != null) // User has a active subscriptions { - // 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 = await GetLimits(subscription); - var subscriptionLimits = - JsonConvert.DeserializeObject(subscription.LimitsJson) - ?? Array.Empty(); + var subscriptionLimit = subscriptionLimits + .FirstOrDefault(x => x.Identifier == identifier); - var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier); - - if (foundLimit != null) - return foundLimit; + 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 - // If the default subscription limit with identifier is found, return it. if not, return empty - return defaultLimits.FirstOrDefault(x => x.Identifier == identifier) ?? new() + 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 async Task GetCurrentUser() + private Task EnsureData(User u) { - var user = await IdentityService.Get(); - - if (user == null) - return null; - - var userWithData = UserRepository + var user = UserRepository .Get() .Include(x => x.CurrentSubscription) - .First(x => x.Id == user.Id); + .First(x => x.Id == u.Id); - return userWithData; - } - - private async Task GetDefaultLimits() // Add cache and reload option - { - var defaultSubscriptionJson = "[]"; - - if (File.Exists(PathBuilder.File("storage", "configs", "default_subscription.json"))) - { - defaultSubscriptionJson = - await File.ReadAllTextAsync(PathBuilder.File("storage", "configs", "default_subscription.json")); - } - - return JsonConvert.DeserializeObject(defaultSubscriptionJson) ?? Array.Empty(); + return Task.FromResult(user); } } \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index cc520db6..9ef2f159 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -53,6 +53,7 @@ + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 1a5160a3..6a414261 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -1,6 +1,5 @@ using BlazorDownloadFile; using BlazorTable; -using CurrieTechnologies.Razor.SweetAlert2; using HealthChecks.UI.Client; using Moonlight.App.ApiClients.CloudPanel; using Moonlight.App.ApiClients.Daemon; @@ -32,6 +31,8 @@ using Moonlight.App.Services.SupportChat; using Sentry; using Serilog; using Serilog.Events; +using Stripe; +using SubscriptionService = Moonlight.App.Services.SubscriptionService; namespace Moonlight { @@ -209,9 +210,9 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - + builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -252,6 +253,10 @@ namespace Moonlight builder.Services.AddBlazorContextMenu(); builder.Services.AddBlazorDownloadFile(); + StripeConfiguration.ApiKey = configService + .Get() + .Moonlight.Stripe.ApiKey; + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor b/Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor index 7291544b..6f049421 100644 --- a/Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor +++ b/Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor @@ -6,6 +6,7 @@ @using Moonlight.App.ApiClients.Modrinth @using Moonlight.App.ApiClients.Wings @using Moonlight.App.Helpers +@using Stripe @inherits ErrorBoundaryBase @inject AlertService AlertService @@ -105,6 +106,13 @@ else { 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 { Logger.Warn(exception); diff --git a/Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor b/Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor index 87dd733c..40dfe36e 100644 --- a/Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor +++ b/Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor @@ -4,10 +4,11 @@ @using Moonlight.App.Repositories @using Moonlight.App.Services @using Moonlight.App.Database.Entities +@using Mappy.Net @inject NavigationManager NavigationManager @inject SubscriptionRepository SubscriptionRepository -@inject SubscriptionAdminService SubscriptionAdminService +@inject SubscriptionService SubscriptionService
@@ -31,7 +32,37 @@ Description
- + +
+ +
+ +
+ +
+ +
+ +
+
@@ -141,12 +172,10 @@ private async Task OnSubmit() { - Subscription!.Name = Model.Name; - Subscription.Description = Model.Description; - - SubscriptionRepository.Update(Subscription); + Subscription = Mapper.Map(Subscription, Model); + await SubscriptionService.Update(Subscription!); - await SubscriptionAdminService.SaveLimits(Subscription, Limits.ToArray()); + await SubscriptionService.UpdateLimits(Subscription!, Limits.ToArray()); NavigationManager.NavigateTo("/admin/subscriptions"); } @@ -159,10 +188,9 @@ if (Subscription != null) { - Model.Name = Subscription.Name; - Model.Description = Subscription.Description; - - Limits = (await SubscriptionAdminService.GetLimits(Subscription)).ToList(); + Model = Mapper.Map(Subscription); + + Limits = (await SubscriptionService.GetLimits(Subscription)).ToList(); } } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Subscriptions/Index.razor b/Moonlight/Shared/Views/Admin/Subscriptions/Index.razor index e7182f6a..f15b2def 100644 --- a/Moonlight/Shared/Views/Admin/Subscriptions/Index.razor +++ b/Moonlight/Shared/Views/Admin/Subscriptions/Index.razor @@ -7,10 +7,7 @@ @inject SmartTranslateService SmartTranslateService @inject SubscriptionRepository SubscriptionRepository - -@inject SubscriptionAdminService SubscriptionAdminService -@inject AlertService AlertService -@inject ClipboardService ClipboardService +@inject SubscriptionService SubscriptionService
@@ -34,7 +31,10 @@ - + + + +