From 6fd1336f1ce2af559e7a3247e848b98e3db81b88 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sat, 27 Jan 2024 13:28:09 +0100 Subject: [PATCH] Added base models for servers. Added ws packet connection utility. Added some ui from old branch. Added some packeges. And more smaller things --- ...240127110558_AddedServerModels.Designer.cs | 1026 +++++++++++++++++ .../20240127110558_AddedServerModels.cs | 266 +++++ .../Migrations/DataContextModelSnapshot.cs | 673 ++++++++--- Moonlight/Core/Helpers/HttpApiClient.cs | 117 ++ Moonlight/Core/Helpers/WsPacketConnection.cs | 27 +- Moonlight/Core/Models/Enums/Permission.cs | 1 + .../Core/Services/Interop/ClipboardService.cs | 18 + .../Features/Servers/Actions/ServerActions.cs | 155 +++ .../Features/Servers/Actions/ServerConfig.cs | 26 + .../Actions/ServerServiceDefinition.cs | 20 + Moonlight/Features/Servers/Entities/Server.cs | 2 - .../Features/Servers/Entities/ServerImage.cs | 2 + .../Features/Servers/Entities/ServerNode.cs | 2 + .../Servers/Exceptions/NodeException.cs | 16 + .../Servers/Extensions/ServerExtensions.cs | 11 + .../Features/Servers/Helpers/MetaCache.cs | 27 + .../Http/Controllers/NodeController.cs | 49 +- .../Http/Controllers/ServersControllers.cs | 57 +- .../Servers/Http/Middleware/NodeMiddleware.cs | 13 +- .../ServerInstallConfiguration.cs | 8 + .../Servers/Models/Abstractions/ServerMeta.cs | 9 + .../Servers/Models/Enums/PowerAction.cs | 9 + .../Servers/Models/Enums/ServerState.cs | 11 + .../Models/Forms/Admin/CreateNodeForm.cs | 23 + .../Models/Forms/Admin/UpdateNodeForm.cs | 23 + .../Models/Packets/ServerOutputMessage.cs | 7 + .../Models/Packets/ServerStateUpdate.cs | 9 + .../Features/Servers/Services/NodeService.cs | 25 +- .../Servers/Services/ServerService.cs | 55 + .../Components/AdminServersNavigation.razor | 21 + .../Servers/UI/Components/Terminal.razor | 85 ++ .../Servers/UI/Layouts/AdminLayout.razor | 5 + .../Servers/UI/Layouts/UserLayout.razor | 289 +++++ .../Servers/UI/Views/Admin/Index.razor | 72 ++ .../Servers/UI/Views/Admin/Nodes/View.razor | 7 + Moonlight/Moonlight.csproj | 6 +- Moonlight/Program.cs | 7 +- Moonlight/wwwroot/js/moonlight.js | 33 + 38 files changed, 2991 insertions(+), 221 deletions(-) create mode 100644 Moonlight/Core/Database/Migrations/20240127110558_AddedServerModels.Designer.cs create mode 100644 Moonlight/Core/Database/Migrations/20240127110558_AddedServerModels.cs create mode 100644 Moonlight/Core/Helpers/HttpApiClient.cs create mode 100644 Moonlight/Core/Services/Interop/ClipboardService.cs create mode 100644 Moonlight/Features/Servers/Actions/ServerActions.cs create mode 100644 Moonlight/Features/Servers/Actions/ServerConfig.cs create mode 100644 Moonlight/Features/Servers/Actions/ServerServiceDefinition.cs create mode 100644 Moonlight/Features/Servers/Exceptions/NodeException.cs create mode 100644 Moonlight/Features/Servers/Helpers/MetaCache.cs create mode 100644 Moonlight/Features/Servers/Models/Abstractions/ServerInstallConfiguration.cs create mode 100644 Moonlight/Features/Servers/Models/Abstractions/ServerMeta.cs create mode 100644 Moonlight/Features/Servers/Models/Enums/PowerAction.cs create mode 100644 Moonlight/Features/Servers/Models/Enums/ServerState.cs create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/CreateNodeForm.cs create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/UpdateNodeForm.cs create mode 100644 Moonlight/Features/Servers/Models/Packets/ServerOutputMessage.cs create mode 100644 Moonlight/Features/Servers/Models/Packets/ServerStateUpdate.cs create mode 100644 Moonlight/Features/Servers/Services/ServerService.cs create mode 100644 Moonlight/Features/Servers/UI/Components/AdminServersNavigation.razor create mode 100644 Moonlight/Features/Servers/UI/Components/Terminal.razor create mode 100644 Moonlight/Features/Servers/UI/Layouts/AdminLayout.razor create mode 100644 Moonlight/Features/Servers/UI/Layouts/UserLayout.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Index.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Nodes/View.razor diff --git a/Moonlight/Core/Database/Migrations/20240127110558_AddedServerModels.Designer.cs b/Moonlight/Core/Database/Migrations/20240127110558_AddedServerModels.Designer.cs new file mode 100644 index 00000000..0fb9d139 --- /dev/null +++ b/Moonlight/Core/Database/Migrations/20240127110558_AddedServerModels.Designer.cs @@ -0,0 +1,1026 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.Core.Database; + +#nullable disable + +namespace Moonlight.Core.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240127110558_AddedServerModels")] + partial class AddedServerModels + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.WordFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WordFilters"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Cpu") + .HasColumnType("INTEGER"); + + b.Property("Disk") + .HasColumnType("INTEGER"); + + b.Property("DockerImageIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("MainAllocationId") + .HasColumnType("INTEGER"); + + b.Property("Memory") + .HasColumnType("INTEGER"); + + b.Property("NodeId") + .HasColumnType("INTEGER"); + + b.Property("OverrideStartupCommand") + .HasColumnType("TEXT"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("ServerNodeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("ServerNodeId"); + + b.ToTable("ServerAllocations"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoPull") + .HasColumnType("INTEGER"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerImageId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerImageId"); + + b.ToTable("ServerDockerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllocationsNeeded") + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DefaultDockerImageIndex") + .HasColumnType("INTEGER"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InstallShell") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OnlineDetection") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParseConfigurations") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartupCommand") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdateUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ServerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowUserToEdit") + .HasColumnType("INTEGER"); + + b.Property("AllowUserToView") + .HasColumnType("INTEGER"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ServerImageVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FtpPort") + .HasColumnType("INTEGER"); + + b.Property("HttpPort") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UseSsl") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerNodes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("Suspended") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.ServiceShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.HasIndex("UserId"); + + b.ToTable("ServiceShares"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CouponId"); + + b.HasIndex("UserId"); + + b.ToTable("CouponUses"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GiftCodeId"); + + b.HasIndex("UserId"); + + b.ToTable("GiftCodeUses"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.Features.Theming.Entities.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CssUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("JsUrl") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Themes"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Open") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("Tries") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsSupport") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("TicketId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketMessages"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostComment", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Community.Entities.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostLike", b => + { + b.HasOne("Moonlight.Features.Community.Entities.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.Core.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerAllocation", "MainAllocation") + .WithMany() + .HasForeignKey("MainAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Node"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", null) + .WithMany("Allocations") + .HasForeignKey("ServerNodeId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("DockerImages") + .HasForeignKey("ServerImageId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.StoreSystem.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.ServiceShare", b => + { + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", null) + .WithMany("Shares") + .HasForeignKey("ServiceId"); + + b.HasOne("Moonlight.Core.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.CouponUse", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("CouponUses") + .HasForeignKey("UserId"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCodeUse", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.GiftCode", "GiftCode") + .WithMany() + .HasForeignKey("GiftCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("GiftCodeUses") + .HasForeignKey("UserId"); + + b.Navigation("GiftCode"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Product", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Transaction", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("Transactions") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId"); + + b.Navigation("Creator"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.TicketMessage", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.HasOne("Moonlight.Features.Ticketing.Entities.Ticket", null) + .WithMany("Messages") + .HasForeignKey("TicketId"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => + { + b.Navigation("CouponUses"); + + b.Navigation("GiftCodeUses"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Navigation("DockerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.Navigation("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/Core/Database/Migrations/20240127110558_AddedServerModels.cs b/Moonlight/Core/Database/Migrations/20240127110558_AddedServerModels.cs new file mode 100644 index 00000000..7c53f5d0 --- /dev/null +++ b/Moonlight/Core/Database/Migrations/20240127110558_AddedServerModels.cs @@ -0,0 +1,266 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.Core.Database.Migrations +{ + /// + public partial class AddedServerModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ServerImages", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + AllocationsNeeded = table.Column(type: "INTEGER", nullable: false), + StartupCommand = table.Column(type: "TEXT", nullable: false), + StopCommand = table.Column(type: "TEXT", nullable: false), + OnlineDetection = table.Column(type: "TEXT", nullable: false), + ParseConfigurations = table.Column(type: "TEXT", nullable: false), + InstallDockerImage = table.Column(type: "TEXT", nullable: false), + InstallShell = table.Column(type: "TEXT", nullable: false), + InstallScript = table.Column(type: "TEXT", nullable: false), + Author = table.Column(type: "TEXT", nullable: false), + DonateUrl = table.Column(type: "TEXT", nullable: true), + UpdateUrl = table.Column(type: "TEXT", nullable: true), + DefaultDockerImageIndex = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServerImages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServerImageVariables", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Key = table.Column(type: "TEXT", nullable: false), + DefaultValue = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + AllowUserToEdit = table.Column(type: "INTEGER", nullable: false), + AllowUserToView = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServerImageVariables", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServerNodes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + Fqdn = table.Column(type: "TEXT", nullable: false), + UseSsl = table.Column(type: "INTEGER", nullable: false), + Token = table.Column(type: "TEXT", nullable: false), + HttpPort = table.Column(type: "INTEGER", nullable: false), + FtpPort = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServerNodes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServerDockerImages", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: false), + AutoPull = table.Column(type: "INTEGER", nullable: false), + ServerImageId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ServerDockerImages", x => x.Id); + table.ForeignKey( + name: "FK_ServerDockerImages_ServerImages_ServerImageId", + column: x => x.ServerImageId, + principalTable: "ServerImages", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ServerAllocations", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + IpAddress = table.Column(type: "TEXT", nullable: false), + Port = table.Column(type: "INTEGER", nullable: false), + ServerId = table.Column(type: "INTEGER", nullable: true), + ServerNodeId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ServerAllocations", x => x.Id); + table.ForeignKey( + name: "FK_ServerAllocations_ServerNodes_ServerNodeId", + column: x => x.ServerNodeId, + principalTable: "ServerNodes", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Servers", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ServiceId = table.Column(type: "INTEGER", nullable: false), + Cpu = table.Column(type: "INTEGER", nullable: false), + Memory = table.Column(type: "INTEGER", nullable: false), + Disk = table.Column(type: "INTEGER", nullable: false), + ImageId = table.Column(type: "INTEGER", nullable: false), + DockerImageIndex = table.Column(type: "INTEGER", nullable: false), + OverrideStartupCommand = table.Column(type: "TEXT", nullable: true), + NodeId = table.Column(type: "INTEGER", nullable: false), + MainAllocationId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Servers", x => x.Id); + table.ForeignKey( + name: "FK_Servers_ServerAllocations_MainAllocationId", + column: x => x.MainAllocationId, + principalTable: "ServerAllocations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Servers_ServerImages_ImageId", + column: x => x.ImageId, + principalTable: "ServerImages", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Servers_ServerNodes_NodeId", + column: x => x.NodeId, + principalTable: "ServerNodes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Servers_Services_ServiceId", + column: x => x.ServiceId, + principalTable: "Services", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ServerVariables", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Key = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: false), + ServerId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ServerVariables", x => x.Id); + table.ForeignKey( + name: "FK_ServerVariables_Servers_ServerId", + column: x => x.ServerId, + principalTable: "Servers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_ServerAllocations_ServerId", + table: "ServerAllocations", + column: "ServerId"); + + migrationBuilder.CreateIndex( + name: "IX_ServerAllocations_ServerNodeId", + table: "ServerAllocations", + column: "ServerNodeId"); + + migrationBuilder.CreateIndex( + name: "IX_ServerDockerImages_ServerImageId", + table: "ServerDockerImages", + column: "ServerImageId"); + + migrationBuilder.CreateIndex( + name: "IX_Servers_ImageId", + table: "Servers", + column: "ImageId"); + + migrationBuilder.CreateIndex( + name: "IX_Servers_MainAllocationId", + table: "Servers", + column: "MainAllocationId"); + + migrationBuilder.CreateIndex( + name: "IX_Servers_NodeId", + table: "Servers", + column: "NodeId"); + + migrationBuilder.CreateIndex( + name: "IX_Servers_ServiceId", + table: "Servers", + column: "ServiceId"); + + migrationBuilder.CreateIndex( + name: "IX_ServerVariables_ServerId", + table: "ServerVariables", + column: "ServerId"); + + migrationBuilder.AddForeignKey( + name: "FK_ServerAllocations_Servers_ServerId", + table: "ServerAllocations", + column: "ServerId", + principalTable: "Servers", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ServerAllocations_ServerNodes_ServerNodeId", + table: "ServerAllocations"); + + migrationBuilder.DropForeignKey( + name: "FK_Servers_ServerNodes_NodeId", + table: "Servers"); + + migrationBuilder.DropForeignKey( + name: "FK_ServerAllocations_Servers_ServerId", + table: "ServerAllocations"); + + migrationBuilder.DropTable( + name: "ServerDockerImages"); + + migrationBuilder.DropTable( + name: "ServerImageVariables"); + + migrationBuilder.DropTable( + name: "ServerVariables"); + + migrationBuilder.DropTable( + name: "ServerNodes"); + + migrationBuilder.DropTable( + name: "Servers"); + + migrationBuilder.DropTable( + name: "ServerAllocations"); + + migrationBuilder.DropTable( + name: "ServerImages"); + } + } +} diff --git a/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs index 0e1f2dc9..fbc8ffb5 100644 --- a/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Moonlight.Core.Database; -using Moonlight.Core.Database; #nullable disable @@ -18,7 +17,52 @@ namespace Moonlight.Core.Database.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Community.Post", b => + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -51,7 +95,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("Posts"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Community.PostComment", b => + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostComment", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -82,7 +126,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("PostComments"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Community.PostLike", b => + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostLike", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -106,7 +150,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("PostLikes"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Community.WordFilter", b => + modelBuilder.Entity("Moonlight.Features.Community.Entities.WordFilter", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -121,7 +165,313 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("WordFilters"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Category", b => + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Cpu") + .HasColumnType("INTEGER"); + + b.Property("Disk") + .HasColumnType("INTEGER"); + + b.Property("DockerImageIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("MainAllocationId") + .HasColumnType("INTEGER"); + + b.Property("Memory") + .HasColumnType("INTEGER"); + + b.Property("NodeId") + .HasColumnType("INTEGER"); + + b.Property("OverrideStartupCommand") + .HasColumnType("TEXT"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("ServerNodeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("ServerNodeId"); + + b.ToTable("ServerAllocations"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoPull") + .HasColumnType("INTEGER"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerImageId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerImageId"); + + b.ToTable("ServerDockerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllocationsNeeded") + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DefaultDockerImageIndex") + .HasColumnType("INTEGER"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InstallShell") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OnlineDetection") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParseConfigurations") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartupCommand") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdateUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ServerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowUserToEdit") + .HasColumnType("INTEGER"); + + b.Property("AllowUserToView") + .HasColumnType("INTEGER"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ServerImageVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FtpPort") + .HasColumnType("INTEGER"); + + b.Property("HttpPort") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UseSsl") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerNodes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("Suspended") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.ServiceShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.HasIndex("UserId"); + + b.ToTable("ServiceShares"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -144,7 +494,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("Categories"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Coupon", b => + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Coupon", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -165,7 +515,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("Coupons"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.CouponUse", b => + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.CouponUse", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -186,7 +536,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("CouponUses"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.GiftCode", b => + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCode", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -207,7 +557,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("GiftCodes"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.GiftCodeUse", b => + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCodeUse", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -228,7 +578,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("GiftCodeUses"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Product", b => + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Product", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -278,64 +628,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("Products"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Service", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConfigJsonOverride") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Nickname") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnType("INTEGER"); - - b.Property("ProductId") - .HasColumnType("INTEGER"); - - b.Property("RenewAt") - .HasColumnType("TEXT"); - - b.Property("Suspended") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.HasIndex("ProductId"); - - b.ToTable("Services"); - }); - - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.ServiceShare", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ServiceId") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ServiceId"); - - b.HasIndex("UserId"); - - b.ToTable("ServiceShares"); - }); - - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Transaction", b => + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Transaction", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -361,7 +654,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("Transaction"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Theme", b => + modelBuilder.Entity("Moonlight.Features.Theming.Entities.Theme", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -393,7 +686,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("Themes"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Tickets.Ticket", b => + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -435,7 +728,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("Tickets"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Tickets.TicketMessage", b => + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.TicketMessage", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -469,52 +762,7 @@ namespace Moonlight.Core.Database.Migrations b.ToTable("TicketMessages"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Avatar") - .HasColumnType("TEXT"); - - b.Property("Balance") - .HasColumnType("REAL"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Email") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Flags") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Permissions") - .HasColumnType("INTEGER"); - - b.Property("TokenValidTimestamp") - .HasColumnType("TEXT"); - - b.Property("TotpKey") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Moonlight.Core.Database.Entities.Community.Post", b => + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => { b.HasOne("Moonlight.Core.Database.Entities.User", "Author") .WithMany() @@ -525,7 +773,7 @@ namespace Moonlight.Core.Database.Migrations b.Navigation("Author"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Community.PostComment", b => + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostComment", b => { b.HasOne("Moonlight.Core.Database.Entities.User", "Author") .WithMany() @@ -533,16 +781,16 @@ namespace Moonlight.Core.Database.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Moonlight.Core.Database.Entities.Community.Post", null) + b.HasOne("Moonlight.Features.Community.Entities.Post", null) .WithMany("Comments") .HasForeignKey("PostId"); b.Navigation("Author"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Community.PostLike", b => + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostLike", b => { - b.HasOne("Moonlight.Core.Database.Entities.Community.Post", null) + b.HasOne("Moonlight.Features.Community.Entities.Post", null) .WithMany("Likes") .HasForeignKey("PostId"); @@ -555,48 +803,67 @@ namespace Moonlight.Core.Database.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.CouponUse", b => + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => { - b.HasOne("Moonlight.Core.Database.Entities.Store.Coupon", "Coupon") + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", "Image") .WithMany() - .HasForeignKey("CouponId") + .HasForeignKey("ImageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Moonlight.Core.Database.Entities.User", null) - .WithMany("CouponUses") - .HasForeignKey("UserId"); - - b.Navigation("Coupon"); - }); - - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.GiftCodeUse", b => - { - b.HasOne("Moonlight.Core.Database.Entities.Store.GiftCode", "GiftCode") + b.HasOne("Moonlight.Features.Servers.Entities.ServerAllocation", "MainAllocation") .WithMany() - .HasForeignKey("GiftCodeId") + .HasForeignKey("MainAllocationId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Moonlight.Core.Database.Entities.User", null) - .WithMany("GiftCodeUses") - .HasForeignKey("UserId"); - - b.Navigation("GiftCode"); - }); - - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Product", b => - { - b.HasOne("Moonlight.Core.Database.Entities.Store.Category", "Category") + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", "Node") .WithMany() - .HasForeignKey("CategoryId") + .HasForeignKey("NodeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Category"); + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Node"); + + b.Navigation("Service"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Service", b => + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", null) + .WithMany("Allocations") + .HasForeignKey("ServerNodeId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("DockerImages") + .HasForeignKey("ServerImageId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => { b.HasOne("Moonlight.Core.Database.Entities.User", "Owner") .WithMany() @@ -604,7 +871,7 @@ namespace Moonlight.Core.Database.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Moonlight.Core.Database.Entities.Store.Product", "Product") + b.HasOne("Moonlight.Features.StoreSystem.Entities.Product", "Product") .WithMany() .HasForeignKey("ProductId") .OnDelete(DeleteBehavior.Cascade) @@ -615,9 +882,9 @@ namespace Moonlight.Core.Database.Migrations b.Navigation("Product"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.ServiceShare", b => + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.ServiceShare", b => { - b.HasOne("Moonlight.Core.Database.Entities.Store.Service", null) + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", null) .WithMany("Shares") .HasForeignKey("ServiceId"); @@ -630,14 +897,55 @@ namespace Moonlight.Core.Database.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Transaction", b => + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.CouponUse", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("CouponUses") + .HasForeignKey("UserId"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCodeUse", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.GiftCode", "GiftCode") + .WithMany() + .HasForeignKey("GiftCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("GiftCodeUses") + .HasForeignKey("UserId"); + + b.Navigation("GiftCode"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Product", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Transaction", b => { b.HasOne("Moonlight.Core.Database.Entities.User", null) .WithMany("Transactions") .HasForeignKey("UserId"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Tickets.Ticket", b => + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => { b.HasOne("Moonlight.Core.Database.Entities.User", "Creator") .WithMany() @@ -645,7 +953,7 @@ namespace Moonlight.Core.Database.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Moonlight.Core.Database.Entities.Store.Service", "Service") + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", "Service") .WithMany() .HasForeignKey("ServiceId"); @@ -654,36 +962,19 @@ namespace Moonlight.Core.Database.Migrations b.Navigation("Service"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Tickets.TicketMessage", b => + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.TicketMessage", b => { b.HasOne("Moonlight.Core.Database.Entities.User", "Sender") .WithMany() .HasForeignKey("SenderId"); - b.HasOne("Moonlight.Core.Database.Entities.Tickets.Ticket", null) + b.HasOne("Moonlight.Features.Ticketing.Entities.Ticket", null) .WithMany("Messages") .HasForeignKey("TicketId"); b.Navigation("Sender"); }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.Community.Post", b => - { - b.Navigation("Comments"); - - b.Navigation("Likes"); - }); - - modelBuilder.Entity("Moonlight.Core.Database.Entities.Store.Service", b => - { - b.Navigation("Shares"); - }); - - modelBuilder.Entity("Moonlight.Core.Database.Entities.Tickets.Ticket", b => - { - b.Navigation("Messages"); - }); - modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => { b.Navigation("CouponUses"); @@ -692,6 +983,40 @@ namespace Moonlight.Core.Database.Migrations b.Navigation("Transactions"); }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Navigation("DockerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.Navigation("Messages"); + }); #pragma warning restore 612, 618 } } diff --git a/Moonlight/Core/Helpers/HttpApiClient.cs b/Moonlight/Core/Helpers/HttpApiClient.cs new file mode 100644 index 00000000..c16084d7 --- /dev/null +++ b/Moonlight/Core/Helpers/HttpApiClient.cs @@ -0,0 +1,117 @@ +using System.Net.Http.Headers; +using System.Text; +using Newtonsoft.Json; + +namespace Moonlight.Core.Helpers; + +public class HttpApiClient : IDisposable where TException : Exception +{ + private readonly HttpClient Client; + private readonly string BaseUrl; + + public HttpApiClient(string baseUrl, string token) + { + Client = new(); + Client.DefaultRequestHeaders.Add("Authorization", token); + + BaseUrl = baseUrl.EndsWith("/") ? baseUrl : baseUrl + "/"; + } + + public async Task Send(HttpMethod method, string path, string? body = null, + string contentType = "text/plain") + { + var request = new HttpRequestMessage(); + + request.RequestUri = new Uri(BaseUrl + path); + request.Method = method; + + if (body != null) + request.Content = new StringContent(body, Encoding.UTF8, new MediaTypeHeaderValue(contentType)); + + var response = await Client.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + await HandleRequestError(response, path); + return ""; + } + + return await response.Content.ReadAsStringAsync(); + } + + private async Task HandleRequestError(HttpResponseMessage response, string path) + { + var content = await response.Content.ReadAsStringAsync(); + var message = $"[{path}] ({response.StatusCode}): {content}"; + var exception = Activator.CreateInstance(typeof(TException), message) as Exception; + + throw exception!; + } + + #region GET + + public async Task GetAsString(string path) => + await Send(HttpMethod.Get, path); + + public async Task Get(string path) => + JsonConvert.DeserializeObject(await Send(HttpMethod.Get, path))!; + + #endregion + + #region POST + + public async Task PostAsString(string path, string body, string contentType = "text/plain") => + await Send(HttpMethod.Post, path, body, contentType); + + public async Task Post(string path, object body) => + JsonConvert.DeserializeObject(await Send(HttpMethod.Post, path, JsonConvert.SerializeObject(body), + "application/json"))!; + + public async Task Post(string path, object? body = null) => await Send(HttpMethod.Post, path, + body == null ? "" : JsonConvert.SerializeObject(body)); + + #endregion + + #region PUT + + public async Task PutAsString(string path, string body, string contentType = "text/plain") => + await Send(HttpMethod.Put, path, body, contentType); + + public async Task Put(string path, object body) => + JsonConvert.DeserializeObject(await Send(HttpMethod.Put, path, JsonConvert.SerializeObject(body), + "application/json"))!; + + public async Task Put(string path, object? body = null) => await Send(HttpMethod.Put, path, + body == null ? "" : JsonConvert.SerializeObject(body)); + + #endregion + + #region PATCH + + public async Task PatchAsString(string path, string body, string contentType = "text/plain") => + await Send(HttpMethod.Patch, path, body, contentType); + + public async Task Patch(string path, object body) => + JsonConvert.DeserializeObject(await Send(HttpMethod.Patch, path, JsonConvert.SerializeObject(body), + "application/json"))!; + + public async Task Patch(string path, object? body = null) => await Send(HttpMethod.Patch, path, + body == null ? "" : JsonConvert.SerializeObject(body)); + + #endregion + + #region DELETE + + public async Task DeleteAsString(string path) => + await Send(HttpMethod.Delete, path); + + public async Task Delete(string path) => + JsonConvert.DeserializeObject(await Send(HttpMethod.Delete, path))!; + + #endregion + + public void Dispose() + { + Client.Dispose(); + } +} \ No newline at end of file diff --git a/Moonlight/Core/Helpers/WsPacketConnection.cs b/Moonlight/Core/Helpers/WsPacketConnection.cs index 477d7b6c..c9e9ad98 100644 --- a/Moonlight/Core/Helpers/WsPacketConnection.cs +++ b/Moonlight/Core/Helpers/WsPacketConnection.cs @@ -79,10 +79,35 @@ public class WsPacketConnection return typedPacketType.GetProperty("Data")!.GetValue(typedPacket); } + public async Task Receive() where T : class + { + var o = await Receive(); + + if (o == null) + return default; + + return (T)o; + } + public async Task Close() { if(WebSocket.State == WebSocketState.Open) - await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + await WebSocket.CloseOutputAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + } + + public async Task WaitForClose() + { + var source = new TaskCompletionSource(); + + Task.Run(async () => + { + while (WebSocket.State == WebSocketState.Open) + await Task.Delay(10); + + source.SetResult(); + }); + + await source.Task; } public class RawPacket diff --git a/Moonlight/Core/Models/Enums/Permission.cs b/Moonlight/Core/Models/Enums/Permission.cs index 11a0c4ff..3ab182c8 100644 --- a/Moonlight/Core/Models/Enums/Permission.cs +++ b/Moonlight/Core/Models/Enums/Permission.cs @@ -11,6 +11,7 @@ public enum Permission AdminTickets = 1004, AdminCommunity = 1030, AdminServices = 1050, + AdminServers = 1060, AdminStore = 1900, AdminViewExceptions = 1999, AdminRoot = 2000 diff --git a/Moonlight/Core/Services/Interop/ClipboardService.cs b/Moonlight/Core/Services/Interop/ClipboardService.cs new file mode 100644 index 00000000..6fb7b765 --- /dev/null +++ b/Moonlight/Core/Services/Interop/ClipboardService.cs @@ -0,0 +1,18 @@ +using Microsoft.JSInterop; + +namespace Moonlight.Core.Services.Interop; + +public class ClipboardService +{ + private readonly IJSRuntime JsRuntime; + + public ClipboardService(IJSRuntime jsRuntime) + { + JsRuntime = jsRuntime; + } + + public async Task Copy(string content) + { + await JsRuntime.InvokeVoidAsync("moonlight.clipboard.copy", content); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Actions/ServerActions.cs b/Moonlight/Features/Servers/Actions/ServerActions.cs new file mode 100644 index 00000000..1edec7b0 --- /dev/null +++ b/Moonlight/Features/Servers/Actions/ServerActions.cs @@ -0,0 +1,155 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.Core.Exceptions; +using Moonlight.Core.Helpers; +using Moonlight.Core.Repositories; +using Moonlight.Features.Servers.Entities; +using Moonlight.Features.Servers.Models.Enums; +using Moonlight.Features.Servers.Services; +using Moonlight.Features.ServiceManagement.Entities; +using Moonlight.Features.ServiceManagement.Models.Abstractions; +using Newtonsoft.Json; + +namespace Moonlight.Features.Servers.Actions; + +public class ServerActions : ServiceActions +{ + public override async Task Create(IServiceProvider provider, Service service) + { + // Load all dependencies from the di + var serverRepo = provider.GetRequiredService>(); + var imageRepo = provider.GetRequiredService>(); + var nodeRepo = provider.GetRequiredService>(); + var allocationRepo = provider.GetRequiredService>(); + var serverService = provider.GetRequiredService(); + + // Parse the configuration file + var config = + JsonConvert.DeserializeObject(service.ConfigJsonOverride ?? service.Product.ConfigJson)!; + + // Load and validate image + + var image = imageRepo + .Get() + .Include(x => x.DockerImages) + .Include(x => x.Variables) + .FirstOrDefault(x => x.Id == config.ImageId); + + if (image == null) + throw new DisplayException("An image with this is is not found"); + + // Load and validate node + + ServerNode? node = null; + + if (config.NodeId != 0) + { + node = nodeRepo + .Get() + .FirstOrDefault(x => x.Id == config.NodeId); + } + + if (node == null) + { + //TODO: Implement auto deploy + throw new DisplayException("Auto deploy has not been implemented yet. Please specify the node id in the product configuration"); + } + + // Load and validate server allocations + ServerAllocation[] allocations = Array.Empty(); + + if (config.DedicatedIp) + { + throw new DisplayException("The dedicated ip mode has not been implemented yet. Please disable the dedicated ip option in the product configuration"); + } + else + { + allocations = allocationRepo + .Get() + .FromSqlRaw( + $"SELECT * FROM `ServerAllocations` WHERE ServerId IS NULL AND ServerNodeId={node.Id} LIMIT {image.AllocationsNeeded}") + .ToArray(); + } + + if (allocations.Length < 1 || allocations.Length < image.AllocationsNeeded) + throw new DisplayException($"Not enough free allocations found on node '{node.Name}'"); + + // Build server db model + + var server = new Server() + { + Service = service, + Cpu = config.Cpu, + Memory = config.Memory, + Disk = config.Disk, + Node = node, + MainAllocation = allocations.First(), + Image = image, + OverrideStartupCommand = null, + DockerImageIndex = image.DefaultDockerImageIndex + }; + + // Add allocations + foreach (var allocation in allocations) + server.Allocations.Add(allocation); + + // Add variables + foreach (var variable in image.Variables) + { + server.Variables.Add(new() + { + Key = variable.Key, + Value = variable.DefaultValue + }); + } + + await serverService.Sync(server); + await serverService.SendPowerAction(server, PowerAction.Install); + } + + public override Task Update(IServiceProvider provider, Service service) + { + throw new NotImplementedException(); + } + + public override async Task Delete(IServiceProvider provider, Service service) + { + // Load dependencies from di + var serverRepo = provider.GetRequiredService>(); + var serverService = provider.GetRequiredService(); + var serverVariableRepo = provider.GetRequiredService>(); + + // Load server + var server = serverRepo + .Get() + .Include(x => x.Variables) + .Include(x => x.MainAllocation) + .FirstOrDefault(x => x.Service.Id == service.Id); + + // Check if server already has been deleted + if (server == null) + { + Logger.Warn($"Server for service {service.Id} is missing when trying to delete the service. Maybe it already has been deleted"); + return; + } + + // Notify the node + await serverService.SyncDelete(server); + + // Clear and delete the variables + var variables = server.Variables.ToArray(); + + server.Variables.Clear(); + + serverRepo.Update(server); + + try + { + foreach (var variable in variables) + serverVariableRepo.Delete(variable); + } + catch (Exception) { /* ignored, as we dont want a operation to fail which just deletes some old data */ } + + // Delete the model + serverRepo.Delete(server); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Actions/ServerConfig.cs b/Moonlight/Features/Servers/Actions/ServerConfig.cs new file mode 100644 index 00000000..83b71352 --- /dev/null +++ b/Moonlight/Features/Servers/Actions/ServerConfig.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; + +namespace Moonlight.Features.Servers.Actions; + +public class ServerConfig +{ + [Description("The amount of cpu cores for a server instance. 100% = 1 Core")] + public int Cpu { get; set; } = 100; + + [Description("The amount of memory in megabytes for a server instance")] + public int Memory { get; set; } = 1024; + + [Description("The amount of disk space in megabytes for a server instance")] + public int Disk { get; set; } = 1024; + + [Description("The id of the image to use for a server")] + public int ImageId { get; set; } = 1; + + [Description( + "The id of the node to use for the server. If not set, moonlight will search automaticly for the best node to deploy on")] + public int NodeId { get; set; } = 0; + + [Description( + "This options specifies if moonlight should give the server an allocation which ip has not been used by another server. So the server will has its own ip")] + public bool DedicatedIp { get; set; } = false; +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Actions/ServerServiceDefinition.cs b/Moonlight/Features/Servers/Actions/ServerServiceDefinition.cs new file mode 100644 index 00000000..be0d68cd --- /dev/null +++ b/Moonlight/Features/Servers/Actions/ServerServiceDefinition.cs @@ -0,0 +1,20 @@ +using Moonlight.Core.Helpers; +using Moonlight.Features.Servers.UI.Layouts; +using Moonlight.Features.ServiceManagement.Models.Abstractions; + +namespace Moonlight.Features.Servers.Actions; + +public class ServerServiceDefinition : ServiceDefinition +{ + public override ServiceActions Actions => new ServerActions(); + public override Type ConfigType => typeof(ServerConfig); + public override async Task BuildUserView(ServiceViewContext context) + { + context.Layout = ComponentHelper.FromType(); + } + + public override Task BuildAdminView(ServiceViewContext context) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Entities/Server.cs b/Moonlight/Features/Servers/Entities/Server.cs index abc0e479..b546e130 100644 --- a/Moonlight/Features/Servers/Entities/Server.cs +++ b/Moonlight/Features/Servers/Entities/Server.cs @@ -6,8 +6,6 @@ public class Server { public int Id { get; set; } public Service Service { get; set; } - - public string Name { get; set; } public int Cpu { get; set; } public int Memory { get; set; } diff --git a/Moonlight/Features/Servers/Entities/ServerImage.cs b/Moonlight/Features/Servers/Entities/ServerImage.cs index fe55677f..464df691 100644 --- a/Moonlight/Features/Servers/Entities/ServerImage.cs +++ b/Moonlight/Features/Servers/Entities/ServerImage.cs @@ -20,5 +20,7 @@ public class ServerImage public string? UpdateUrl { get; set; } public List Variables = new(); + + public int DefaultDockerImageIndex { get; set; } = 0; public List DockerImages { get; set; } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Entities/ServerNode.cs b/Moonlight/Features/Servers/Entities/ServerNode.cs index aacc00be..59dd5141 100644 --- a/Moonlight/Features/Servers/Entities/ServerNode.cs +++ b/Moonlight/Features/Servers/Entities/ServerNode.cs @@ -5,6 +5,8 @@ public class ServerNode public int Id { get; set; } public string Name { get; set; } + public string Fqdn { get; set; } + public bool UseSsl { get; set; } public string Token { get; set; } public int HttpPort { get; set; } public int FtpPort { get; set; } diff --git a/Moonlight/Features/Servers/Exceptions/NodeException.cs b/Moonlight/Features/Servers/Exceptions/NodeException.cs new file mode 100644 index 00000000..0991dd89 --- /dev/null +++ b/Moonlight/Features/Servers/Exceptions/NodeException.cs @@ -0,0 +1,16 @@ +namespace Moonlight.Features.Servers.Exceptions; + +public class NodeException : Exception +{ + public NodeException() + { + } + + public NodeException(string message) : base(message) + { + } + + public NodeException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Extensions/ServerExtensions.cs b/Moonlight/Features/Servers/Extensions/ServerExtensions.cs index 89f3f2ff..451d6a75 100644 --- a/Moonlight/Features/Servers/Extensions/ServerExtensions.cs +++ b/Moonlight/Features/Servers/Extensions/ServerExtensions.cs @@ -60,4 +60,15 @@ public static class ServerExtensions return serverConfiguration; } + + public static ServerInstallConfiguration ToServerInstallConfiguration(this Server server) + { + var installConfiguration = new ServerInstallConfiguration(); + + installConfiguration.DockerImage = server.Image.InstallDockerImage; + installConfiguration.Script = server.Image.InstallScript; + installConfiguration.Shell = server.Image.InstallShell; + + return installConfiguration; + } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Helpers/MetaCache.cs b/Moonlight/Features/Servers/Helpers/MetaCache.cs new file mode 100644 index 00000000..de4b5609 --- /dev/null +++ b/Moonlight/Features/Servers/Helpers/MetaCache.cs @@ -0,0 +1,27 @@ +namespace Moonlight.Features.Servers.Helpers; + +public class MetaCache +{ + private readonly Dictionary Cache = new(); + + public Task Update(int id, Action metaAction) + { + lock (Cache) + { + T? meta = default; + + if (Cache.ContainsKey(id)) + meta = Cache[id]; + + if (meta == null) + { + meta = Activator.CreateInstance(); + Cache.Add(id, meta); + } + + metaAction.Invoke(meta); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Http/Controllers/NodeController.cs b/Moonlight/Features/Servers/Http/Controllers/NodeController.cs index 5d4abfe4..a05325f3 100644 --- a/Moonlight/Features/Servers/Http/Controllers/NodeController.cs +++ b/Moonlight/Features/Servers/Http/Controllers/NodeController.cs @@ -1,6 +1,9 @@ +using System.Net.WebSockets; using Microsoft.AspNetCore.Mvc; +using Moonlight.Core.Helpers; using Moonlight.Features.Servers.Entities; using Moonlight.Features.Servers.Extensions.Attributes; +using Moonlight.Features.Servers.Models.Packets; using Moonlight.Features.Servers.Services; namespace Moonlight.Features.Servers.Http.Controllers; @@ -11,10 +14,12 @@ namespace Moonlight.Features.Servers.Http.Controllers; public class NodeController : Controller { private readonly NodeService NodeService; + private readonly ServerService ServerService; - public NodeController(NodeService nodeService) + public NodeController(NodeService nodeService, ServerService serverService) { NodeService = nodeService; + ServerService = serverService; } [HttpPost("notify/start")] @@ -23,7 +28,7 @@ public class NodeController : Controller // Load node from request context var node = (HttpContext.Items["Node"] as ServerNode)!; - await NodeService.UpdateMeta(node, meta => + await NodeService.Meta.Update(node.Id, meta => { meta.IsBooting = true; }); @@ -37,11 +42,47 @@ public class NodeController : Controller // Load node from request context var node = (HttpContext.Items["Node"] as ServerNode)!; - await NodeService.UpdateMeta(node, meta => + await NodeService.Meta.Update(node.Id, meta => { - meta.IsBooting = true; + meta.IsBooting = false; }); return Ok(); } + + [HttpGet("ws")] + public async Task Ws() + { + // Validate if it is even a websocket connection + if (HttpContext.WebSockets.IsWebSocketRequest) + return BadRequest("This endpoint is only available for websockets"); + + // Accept websocket connection + var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); + + // Build connection wrapper + var wsPacketConnection = new WsPacketConnection(websocket); + + // Register packets + await wsPacketConnection.RegisterPacket("serverStateUpdate"); + await wsPacketConnection.RegisterPacket("serverOutputMessage"); + + while (websocket.State == WebSocketState.Open) + { + var packet = await wsPacketConnection.Receive(); + + if (packet is ServerStateUpdate serverStateUpdate) + { + await ServerService.Meta.Update(serverStateUpdate.Id, meta => + { + meta.State = serverStateUpdate.State; + meta.LastChangeTimestamp = DateTime.UtcNow; + }); + } + } + + await wsPacketConnection.Close(); + + return Ok(); + } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Http/Controllers/ServersControllers.cs b/Moonlight/Features/Servers/Http/Controllers/ServersControllers.cs index 4b3eaf35..078af3da 100644 --- a/Moonlight/Features/Servers/Http/Controllers/ServersControllers.cs +++ b/Moonlight/Features/Servers/Http/Controllers/ServersControllers.cs @@ -25,7 +25,7 @@ public class ServersControllers : Controller public async Task GetAllServersWs() { // Validate if it is even a websocket connection - if (HttpContext.WebSockets.IsWebSocketRequest) + if (!HttpContext.WebSockets.IsWebSocketRequest) return BadRequest("This endpoint is only available for websockets"); // Accept websocket connection @@ -33,6 +33,7 @@ public class ServersControllers : Controller // Build connection wrapper var wsPacketConnection = new WsPacketConnection(websocket); + await wsPacketConnection.RegisterPacket("amount"); await wsPacketConnection.RegisterPacket("serverConfiguration"); // Read server data for the node @@ -42,10 +43,9 @@ public class ServersControllers : Controller var servers = ServerRepository .Get() .Include(x => x.Allocations) + .Include(x => x.Variables) .Include(x => x.MainAllocation) .Include(x => x.Image) - .ThenInclude(x => x.Variables) - .Include(x => x.Image) .ThenInclude(x => x.DockerImages) .Where(x => x.Node.Id == node.Id) .ToArray(); @@ -54,14 +54,59 @@ public class ServersControllers : Controller var serverConfigurations = servers .Select(x => x.ToServerConfiguration()) .ToArray(); + + // Send the amount of configs the node will receive + await wsPacketConnection.Send(servers.Length); // Send the server configurations foreach (var serverConfiguration in serverConfigurations) await wsPacketConnection.Send(serverConfiguration); - - // Close the connection - await wsPacketConnection.Close(); + + await wsPacketConnection.WaitForClose(); return Ok(); } + + [HttpGet("{id:int}")] + public async Task> GetServerById(int id) + { + var node = (HttpContext.Items["Node"] as ServerNode)!; + + var server = ServerRepository + .Get() + .Include(x => x.Allocations) + .Include(x => x.MainAllocation) + .Include(x => x.Image) + .ThenInclude(x => x.Variables) + .Include(x => x.Image) + .ThenInclude(x => x.DockerImages) + .Where(x => x.Node.Id == node.Id) + .FirstOrDefault(x => x.Id == id); + + if (server == null) + return NotFound(); + + var configuration = server.ToServerConfiguration(); + + return Ok(configuration); + } + + [HttpGet("{id:int}/install")] + public async Task> GetServerInstallById(int id) + { + var node = (HttpContext.Items["Node"] as ServerNode)!; + + var server = ServerRepository + .Get() + .Include(x => x.Image) + .Where(x => x.Node.Id == node.Id) + .FirstOrDefault(x => x.Id == id); + + if (server == null) + return NotFound(); + + var configuration = server.ToServerInstallConfiguration(); + + return Ok(configuration); + } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Http/Middleware/NodeMiddleware.cs b/Moonlight/Features/Servers/Http/Middleware/NodeMiddleware.cs index 73f679c7..1a1c1527 100644 --- a/Moonlight/Features/Servers/Http/Middleware/NodeMiddleware.cs +++ b/Moonlight/Features/Servers/Http/Middleware/NodeMiddleware.cs @@ -8,12 +8,12 @@ namespace Moonlight.Features.Servers.Http.Middleware; public class NodeMiddleware { private RequestDelegate Next; - private readonly Repository NodeRepository; + private readonly IServiceProvider ServiceProvider; - public NodeMiddleware(RequestDelegate next, Repository nodeRepository) + public NodeMiddleware(RequestDelegate next, IServiceProvider serviceProvider) { Next = next; - NodeRepository = nodeRepository; + ServiceProvider = serviceProvider; } public async Task Invoke(HttpContext context) @@ -57,7 +57,7 @@ public class NodeMiddleware return; } - var token = context.Request.Headers["Authorization"]; + var token = context.Request.Headers["Authorization"].ToString(); // Check if header is null if (string.IsNullOrEmpty(token)) @@ -65,9 +65,12 @@ public class NodeMiddleware context.Response.StatusCode = 403; return; } + + using var scope = ServiceProvider.CreateScope(); + var nodeRepo = scope.ServiceProvider.GetRequiredService>(); // Check if any node has the token specified by the request - var node = NodeRepository + var node = nodeRepo .Get() .FirstOrDefault(x => x.Token == token); diff --git a/Moonlight/Features/Servers/Models/Abstractions/ServerInstallConfiguration.cs b/Moonlight/Features/Servers/Models/Abstractions/ServerInstallConfiguration.cs new file mode 100644 index 00000000..ee4a1eca --- /dev/null +++ b/Moonlight/Features/Servers/Models/Abstractions/ServerInstallConfiguration.cs @@ -0,0 +1,8 @@ +namespace Moonlight.Features.Servers.Models.Abstractions; + +public class ServerInstallConfiguration +{ + public string DockerImage { get; set; } + public string Shell { get; set; } + public string Script { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Abstractions/ServerMeta.cs b/Moonlight/Features/Servers/Models/Abstractions/ServerMeta.cs new file mode 100644 index 00000000..7af0ff74 --- /dev/null +++ b/Moonlight/Features/Servers/Models/Abstractions/ServerMeta.cs @@ -0,0 +1,9 @@ +using Moonlight.Features.Servers.Models.Enums; + +namespace Moonlight.Features.Servers.Models.Abstractions; + +public class ServerMeta +{ + public ServerState State { get; set; } + public DateTime LastChangeTimestamp { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Enums/PowerAction.cs b/Moonlight/Features/Servers/Models/Enums/PowerAction.cs new file mode 100644 index 00000000..097d1bcf --- /dev/null +++ b/Moonlight/Features/Servers/Models/Enums/PowerAction.cs @@ -0,0 +1,9 @@ +namespace Moonlight.Features.Servers.Models.Enums; + +public enum PowerAction +{ + Start, + Stop, + Kill, + Install +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Enums/ServerState.cs b/Moonlight/Features/Servers/Models/Enums/ServerState.cs new file mode 100644 index 00000000..376bb9b8 --- /dev/null +++ b/Moonlight/Features/Servers/Models/Enums/ServerState.cs @@ -0,0 +1,11 @@ +namespace Moonlight.Features.Servers.Models.Enums; + +public enum ServerState +{ + Offline, + Starting, + Online, + Stopping, + Installing, + Join2Start +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Forms/Admin/CreateNodeForm.cs b/Moonlight/Features/Servers/Models/Forms/Admin/CreateNodeForm.cs new file mode 100644 index 00000000..eb328bfd --- /dev/null +++ b/Moonlight/Features/Servers/Models/Forms/Admin/CreateNodeForm.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Features.Servers.Models.Forms.Admin; + +public class CreateNodeForm +{ + [Required(ErrorMessage = "You need to specify a name")] + public string Name { get; set; } + + [Required(ErrorMessage = "You need to specify a fqdn")] + [Description("This needs to be the ip or domain of the node")] + public string Fqdn { get; set; } + + [Description("This enables ssl for the http conenctions to the node. Only enable this if you have the cert installed on the node")] + public bool UseSsl { get; set; } + + [Description("This is the http(s) port used by the node to allow communication to the node from the panel")] + public int HttpPort { get; set; } = 8080; + + [Description("This is the ftp port the panel and the users use to access their servers filesystem")] + public int FtpPort { get; set; } = 2021; +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Forms/Admin/UpdateNodeForm.cs b/Moonlight/Features/Servers/Models/Forms/Admin/UpdateNodeForm.cs new file mode 100644 index 00000000..fdeaf112 --- /dev/null +++ b/Moonlight/Features/Servers/Models/Forms/Admin/UpdateNodeForm.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Features.Servers.Models.Forms.Admin; + +public class UpdateNodeForm +{ + [Required(ErrorMessage = "You need to specify a name")] + public string Name { get; set; } + + [Required(ErrorMessage = "You need to specify a fqdn")] + [Description("This needs to be the ip or domain of the node")] + public string Fqdn { get; set; } + + [Description("This enables ssl for the http conenctions to the node. Only enable this if you have the cert installed on the node")] + public bool UseSsl { get; set; } + + [Description("This is the http(s) port used by the node to allow communication to the node from the panel")] + public int HttpPort { get; set; } = 8080; + + [Description("This is the ftp port the panel and the users use to access their servers filesystem")] + public int FtpPort { get; set; } = 2021; +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Packets/ServerOutputMessage.cs b/Moonlight/Features/Servers/Models/Packets/ServerOutputMessage.cs new file mode 100644 index 00000000..6809dbba --- /dev/null +++ b/Moonlight/Features/Servers/Models/Packets/ServerOutputMessage.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Features.Servers.Models.Packets; + +public class ServerOutputMessage +{ + public int Id { get; set; } + public string Message { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Packets/ServerStateUpdate.cs b/Moonlight/Features/Servers/Models/Packets/ServerStateUpdate.cs new file mode 100644 index 00000000..9b971ab9 --- /dev/null +++ b/Moonlight/Features/Servers/Models/Packets/ServerStateUpdate.cs @@ -0,0 +1,9 @@ +using Moonlight.Features.Servers.Models.Enums; + +namespace Moonlight.Features.Servers.Models.Packets; + +public class ServerStateUpdate +{ + public int Id { get; set; } + public ServerState State { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Services/NodeService.cs b/Moonlight/Features/Servers/Services/NodeService.cs index 03da86c0..5b188686 100644 --- a/Moonlight/Features/Servers/Services/NodeService.cs +++ b/Moonlight/Features/Servers/Services/NodeService.cs @@ -1,30 +1,9 @@ -using Moonlight.Features.Servers.Entities; +using Moonlight.Features.Servers.Helpers; using Moonlight.Features.Servers.Models.Abstractions; namespace Moonlight.Features.Servers.Services; public class NodeService { - private readonly Dictionary MetaCache = new(); - - public Task UpdateMeta(ServerNode node, Action metaAction) - { - lock (MetaCache) - { - NodeMeta? meta = null; - - if (MetaCache.ContainsKey(node.Id)) - meta = MetaCache[node.Id]; - - if (meta == null) - { - meta = new(); - MetaCache.Add(node.Id, meta); - } - - metaAction.Invoke(meta); - } - - return Task.CompletedTask; - } + public readonly MetaCache Meta = new(); } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Services/ServerService.cs b/Moonlight/Features/Servers/Services/ServerService.cs new file mode 100644 index 00000000..54238055 --- /dev/null +++ b/Moonlight/Features/Servers/Services/ServerService.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.Core.Helpers; +using Moonlight.Core.Repositories; +using Moonlight.Features.Servers.Entities; +using Moonlight.Features.Servers.Exceptions; +using Moonlight.Features.Servers.Helpers; +using Moonlight.Features.Servers.Models.Abstractions; +using Moonlight.Features.Servers.Models.Enums; + +namespace Moonlight.Features.Servers.Services; + +public class ServerService +{ + public readonly MetaCache Meta = new(); + + private readonly IServiceProvider ServiceProvider; + + public ServerService(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public async Task Sync(Server server) + { + using var httpClient = CreateHttpClient(server); + await httpClient.Post($"servers/{server.Id}/sync"); + } + + public async Task SyncDelete(Server server) + { + + } + + public async Task SendPowerAction(Server server, PowerAction powerAction) + { + using var httpClient = CreateHttpClient(server); + await httpClient.Post($"servers/{server.Id}/power/{powerAction.ToString().ToLower()}"); + } + + private HttpApiClient CreateHttpClient(Server server) + { + using var scope = ServiceProvider.CreateScope(); + var serverRepo = scope.ServiceProvider.GetRequiredService>(); + + var serverWithNode = serverRepo + .Get() + .Include(x => x.Node) + .First(x => x.Id == server.Id); + + var protocol = serverWithNode.Node.UseSsl ? "https" : "http"; + var remoteUrl = $"{protocol}://{serverWithNode.Node.Fqdn}/"; + + return new HttpApiClient(remoteUrl, serverWithNode.Node.Token); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Components/AdminServersNavigation.razor b/Moonlight/Features/Servers/UI/Components/AdminServersNavigation.razor new file mode 100644 index 00000000..4472d360 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Components/AdminServersNavigation.razor @@ -0,0 +1,21 @@ +
+
+ +
+
+ +@code +{ + [Parameter] public int Index { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Components/Terminal.razor b/Moonlight/Features/Servers/UI/Components/Terminal.razor new file mode 100644 index 00000000..441b0531 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Components/Terminal.razor @@ -0,0 +1,85 @@ +@using XtermBlazor +@using Moonlight.Core.Services.Interop + +@inject ClipboardService ClipboardService +@inject ToastService ToastService + + + +@code +{ + private Xterm Term; + + private readonly TerminalOptions Options = new() + { + CursorBlink = false, + CursorWidth = 0, + Theme = + { + Background = "#000000", + CursorAccent = "#000000", + Cursor = "#000000" + }, + DisableStdin = true, + FontFamily = "monospace" + }; + + private readonly string[] AddonIds = new[] + { + "xterm-addon-fit", + "xterm-addon-web-links", + "xterm-addon-search" + }; + + private bool HasBeenRendered = false; + private readonly List UnRenderedMessageCache = new(); + + public async Task WriteLine(string content) + { + if(HasBeenRendered) + await Term.WriteLine(content); + else + { + lock (UnRenderedMessageCache) + UnRenderedMessageCache.Add(content); + } + } + + private async void OnFirstRender() + { + try + { + await Term.InvokeAddonFunctionVoidAsync("xterm-addon-fit", "fit"); + + // This disables the key handling for xterm completely in order to allow Strg + C copying and other features + Term.AttachCustomKeyEventHandler(key => + { + if (key.CtrlKey && key.Code == "KeyC" && key.Type == "keydown") + { + Task.Run(async () => + { + var content = await Term.GetSelection(); + await ClipboardService.Copy(content); + await ToastService.Info("Copied console selection to clipboard"); + }); + } + + return false; + }); + } + catch (Exception){ /* Ignore all js errors as the addons are not that important to risk a crash of the ui */ } + + string[] messagesToWrite; + + lock (UnRenderedMessageCache) + { + messagesToWrite = UnRenderedMessageCache.ToArray(); + UnRenderedMessageCache.Clear(); + } + + foreach (var message in messagesToWrite) + await Term.WriteLine(message); + + HasBeenRendered = true; + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Layouts/AdminLayout.razor b/Moonlight/Features/Servers/UI/Layouts/AdminLayout.razor new file mode 100644 index 00000000..e1e86d8f --- /dev/null +++ b/Moonlight/Features/Servers/UI/Layouts/AdminLayout.razor @@ -0,0 +1,5 @@ +

AdminLayout

+ +@code { + +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor b/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor new file mode 100644 index 00000000..79d90861 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor @@ -0,0 +1,289 @@ +@using Moonlight.Features.ServiceManagement.Entities +@using Moonlight.Features.ServiceManagement.Models.Abstractions +@using Moonlight.Features.Servers.Entities +@using Moonlight.Features.Servers.Services +@using Moonlight.Core.Repositories +@using Moonlight.Core.Services.Interop +@using Moonlight.Features.Servers.Models.Abstractions +@using Moonlight.Features.Servers.Models.Enums +@using Microsoft.EntityFrameworkCore +@using Moonlight.Core.Helpers +@using Moonlight.Features.Servers.UI.Components + +@inject Repository ServerRepository +@inject ServerService ServerService +@inject ToastService ToastService + +@implements IDisposable + + +
+
+
+
+ + @(Service.Nickname ?? $"Service {Service.Id}") + + + @(Server.Image.Name) + +
+
+
+
+ @{ + var color = "secondary"; + + switch (Meta.State) + { + case ServerState.Stopping: + color = "warning"; + break; + + case ServerState.Starting: + color = "warning"; + break; + + case ServerState.Offline: + color = "danger"; + break; + + case ServerState.Online: + color = "success"; + break; + + case ServerState.Installing: + color = "primary"; + break; + + case ServerState.Join2Start: + color = "info"; + break; + } + } + + + + @(Meta.State) + (@(Formatter.FormatUptime(DateTime.UtcNow - Meta.LastChangeTimestamp))) + +
+
+
+ + + @(Server.Node.Fqdn):@(Server.MainAllocation.Port) + +
+
+ + + 188.75.252.37:10324 + +
+
+
+
+
+
+ @if (Meta.State == ServerState.Offline) + { + + + + } + else + { + + } + + @if (Meta.State == ServerState.Offline || Meta.State == ServerState.Installing) + { + + } + else + { + + + + } + + @if (Meta.State == ServerState.Offline || Meta.State == ServerState.Installing) + { + + } + else + { + + + + } +
+
+
+ +
+ +
+ @if (IsInstalling) + { +
+ +
+ } + else + { + + + + + @foreach (var uiPage in ViewContext.Pages) + { + + @uiPage.Component + + } + + + + + } +
+
+ +@code +{ + [Parameter] + public Service Service { get; set; } + + [Parameter] + public ServiceViewContext ViewContext { get; set; } + + [Parameter] + public string? Route { get; set; } + + private Server Server; + private ServerMeta Meta; + private CancellationTokenSource BackgroundCancel = new(); + + private Terminal? InstallTerminal; + private bool IsInstalling = false; + + private async Task Load(LazyLoader lazyLoader) + { + await lazyLoader.SetText("Loading server information"); + + Server = ServerRepository + .Get() + .Include(x => x.Image) + .Include(x => x.Node) + .Include(x => x.Variables) + .Include(x => x.Allocations) + .Include(x => x.MainAllocation) + .First(x => x.Service.Id == Service.Id); + + /* + + // Load meta and setup event handlers + Meta = await ServerService.Meta.Get(Server); + Meta.OnStateChanged += async Task () => + { + await InvokeAsync(StateHasChanged); + + // Change from offline to installing + // This will trigger the initialisation of the install view + if (Meta.State == ServerState.Installing && !IsInstalling) + { + IsInstalling = true; + + // After this call, we should have access to the install terminal reference + await InvokeAsync(StateHasChanged); + + Meta.OnConsoleMessage += OnInstallConsoleMessage; + } + // Change from installing to offline + // This will trigger the destruction of the install view + else if (Meta.State == ServerState.Offline && IsInstalling) + { + IsInstalling = false; + + Meta.OnConsoleMessage -= OnInstallConsoleMessage; + + // After this call, the install terminal will disappear + await InvokeAsync(StateHasChanged); + + await ToastService.Info("Server installation complete"); + } + }; + + // Send console subscription and add auto resubscribe for it + await ServerService.Console.Subscribe(Server); + + // We need this to revalidate to the daemon that we are still interested + // in the console logs. By default the expiration time is 15 minutes from last + // subscription so every 10 minutes should ensure we are subscribed + Task.Run(async () => + { + while (!BackgroundCancel.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMinutes(10)); + await ServerService.Console.Subscribe(Server); + } + }); + + // In order to update the timer correctly, we are calling a re + Task.Run(async () => + { + while (!BackgroundCancel.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + await InvokeAsync(StateHasChanged); + } + }); + + */ + } + + private async Task OnInstallConsoleMessage(string message) + { + if(InstallTerminal != null) + await InstallTerminal.WriteLine(message); + } + + private async Task Start() => await ServerService.SendPowerAction(Server, PowerAction.Start); + + private async Task Stop() => await ServerService.SendPowerAction(Server, PowerAction.Stop); + + private async Task Kill() => await ServerService.SendPowerAction(Server, PowerAction.Kill); + + public void Dispose() + { + BackgroundCancel.Cancel(); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Index.razor b/Moonlight/Features/Servers/UI/Views/Admin/Index.razor new file mode 100644 index 00000000..9a04626c --- /dev/null +++ b/Moonlight/Features/Servers/UI/Views/Admin/Index.razor @@ -0,0 +1,72 @@ +@page "/admin/servers" + +@using Moonlight.Core.Extensions.Attributes +@using Moonlight.Core.Models.Enums +@using Moonlight.Core.Repositories +@using Moonlight.Features.Servers.UI.Components +@using Moonlight.Features.Servers.Entities +@using Moonlight.Features.Servers.Models.Forms.Admin +@using Microsoft.EntityFrameworkCore +@using Moonlight.Core.Exceptions +@using Moonlight.Core.Helpers +@using BlazorTable + +@attribute [RequirePermission(Permission.AdminServers)] + +@inject Repository NodeRepository + + + + + + + + + + + + + + + + +@code +{ + private ServerNode[] Load(Repository repository) + { + return repository.Get().ToArray(); + } + + private Task ValidateAdd(ServerNode node) + { + // Generate token + node.Token = Formatter.GenerateString(32); + + return Task.CompletedTask; + } + + private Task ValidateDelete(ServerNode n) + { + var nodeHasAllocations = NodeRepository + .Get() + .Include(x => x.Allocations) + .First(x => x.Id == n.Id) + .Allocations + .Any(); + + if (nodeHasAllocations) + throw new DisplayException("The node still has allocations. Delete them in order to delete the node"); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Nodes/View.razor b/Moonlight/Features/Servers/UI/Views/Admin/Nodes/View.razor new file mode 100644 index 00000000..9ab860f4 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Views/Admin/Nodes/View.razor @@ -0,0 +1,7 @@ +@page "/admin/servers/nodes/{Id:int}" + +@code +{ + [Parameter] + public int Id { get; set; } +} diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index c39c194c..fe7432fb 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -34,13 +34,8 @@ - - - - - @@ -68,6 +63,7 @@ + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 91cbd664..8af7466d 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -1,4 +1,5 @@ using BlazorTable; +using Microsoft.AspNetCore.WebSockets; using Moonlight.Core.Database; using Moonlight.Core.Actions.Dummy; using Moonlight.Core.Database; @@ -13,6 +14,7 @@ using Moonlight.Core.Services.Users; using Moonlight.Core.Services.Utils; using Moonlight.Features.Advertisement.Services; using Moonlight.Features.Community.Services; +using Moonlight.Features.Servers.Actions; using Moonlight.Features.Servers.Http.Middleware; using Moonlight.Features.Servers.Services; using Moonlight.Features.ServiceManagement.Entities.Enums; @@ -73,6 +75,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Services / Store builder.Services.AddScoped(); @@ -107,6 +110,7 @@ builder.Services.AddScoped(); // Services / Servers builder.Services.AddSingleton(); +builder.Services.AddSingleton(); // Services builder.Services.AddScoped(); @@ -135,6 +139,7 @@ var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); +app.UseWebSockets(); app.UseMiddleware(); @@ -150,7 +155,7 @@ moonlightService.Application = app; moonlightService.LogPath = logPath; var serviceService = app.Services.GetRequiredService(); -serviceService.Register(ServiceType.Server); +serviceService.Register(ServiceType.Server); await pluginService.RunPrePost(app); diff --git a/Moonlight/wwwroot/js/moonlight.js b/Moonlight/wwwroot/js/moonlight.js index 4a7a6593..60d7c712 100644 --- a/Moonlight/wwwroot/js/moonlight.js +++ b/Moonlight/wwwroot/js/moonlight.js @@ -169,5 +169,38 @@ window.moonlight = { let editor = document.getElementById(id).ckeditorInstance; editor.setData(data); } + }, + clipboard: { + copy: function (text) { + if (!navigator.clipboard) { + var textArea = document.createElement("textarea"); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.top = "0"; + textArea.style.left = "0"; + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + var successful = document.execCommand('copy'); + var msg = successful ? 'successful' : 'unsuccessful'; + } catch (err) { + console.error('Fallback: Oops, unable to copy', err); + } + + document.body.removeChild(textArea); + return; + } + navigator.clipboard.writeText(text).then(function () { + }, + function (err) { + console.error('Async: Could not copy text: ', err); + } + ); + } } } \ No newline at end of file