diff --git a/Moonlight/App/Configuration/ConfigV1.cs b/Moonlight/App/Configuration/ConfigV1.cs index 4c07e7d4..2bf6bed1 100644 --- a/Moonlight/App/Configuration/ConfigV1.cs +++ b/Moonlight/App/Configuration/ConfigV1.cs @@ -38,9 +38,7 @@ public class ConfigV1 [JsonProperty("Mail")] public MailData Mail { 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("Statistics")] public StatisticsData Statistics { get; set; } = new(); @@ -50,6 +48,15 @@ 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")] + [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 @@ -318,11 +325,6 @@ public class ConfigV1 [JsonProperty("Wait")] public long Wait { get; set; } = 15; } - public class SubscriptionsData - { - [JsonProperty("SellPass")] public SellPassData SellPass { get; set; } = new(); - } - public class SellPassData { [JsonProperty("Enable")] public bool Enable { get; set; } = false; 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..9878b736 --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/BillingController.cs @@ -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 Cancel() + { + var user = await IdentityService.Get(); + + if (user == null) + return Redirect("/login"); + + return Redirect("/profile/subscriptions/close"); + } + + [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/Background/DiscordNotificationService.cs b/Moonlight/App/Services/Background/DiscordNotificationService.cs index f4257d52..014b9c66 100644 --- a/Moonlight/App/Services/Background/DiscordNotificationService.cs +++ b/Moonlight/App/Services/Background/DiscordNotificationService.cs @@ -35,6 +35,7 @@ public class DiscordNotificationService Event.On("supportChat.message", this, OnSupportChatMessage); Event.On("supportChat.close", this, OnSupportChatClose); Event.On("user.rating", this, OnUserRated); + Event.On("billing.completed", this, OnBillingCompleted); } 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) { await SendNotification("", builder => diff --git a/Moonlight/App/Services/BillingService.cs b/Moonlight/App/Services/BillingService.cs new file mode 100644 index 00000000..9b13af1a --- /dev/null +++ b/Moonlight/App/Services/BillingService.cs @@ -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 SubscriptionRepository; + private readonly SessionServerService SessionServerService; + private readonly EventSystem Event; + private readonly MailService MailService; + + public BillingService( + ConfigService configService, + SubscriptionService subscriptionService, + Repository subscriptionRepository, + EventSystem eventSystem, + SessionServerService sessionServerService, + MailService mailService) + { + ConfigService = configService; + SubscriptionService = subscriptionService; + SubscriptionRepository = subscriptionRepository; + Event = eventSystem; + SessionServerService = sessionServerService; + MailService = mailService; + } + + 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 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); + } +} \ 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 95436817..60c742bd 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; @@ -33,6 +32,8 @@ using Moonlight.App.Services.SupportChat; using Sentry; using Serilog; using Serilog.Events; +using Stripe; +using SubscriptionService = Moonlight.App.Services.SubscriptionService; namespace Moonlight { @@ -208,9 +209,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(); @@ -251,6 +252,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 @@ - + + + +