Added base models for servers. Added ws packet connection utility. Added some ui from old branch. Added some packeges. And more smaller things

This commit is contained in:
Marcel Baumgartner 2024-01-27 13:28:09 +01:00
parent 33c1ffa0ba
commit 6fd1336f1c
38 changed files with 2991 additions and 221 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,266 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.Core.Database.Migrations
{
/// <inheritdoc />
public partial class AddedServerModels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ServerImages",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
AllocationsNeeded = table.Column<int>(type: "INTEGER", nullable: false),
StartupCommand = table.Column<string>(type: "TEXT", nullable: false),
StopCommand = table.Column<string>(type: "TEXT", nullable: false),
OnlineDetection = table.Column<string>(type: "TEXT", nullable: false),
ParseConfigurations = table.Column<string>(type: "TEXT", nullable: false),
InstallDockerImage = table.Column<string>(type: "TEXT", nullable: false),
InstallShell = table.Column<string>(type: "TEXT", nullable: false),
InstallScript = table.Column<string>(type: "TEXT", nullable: false),
Author = table.Column<string>(type: "TEXT", nullable: false),
DonateUrl = table.Column<string>(type: "TEXT", nullable: true),
UpdateUrl = table.Column<string>(type: "TEXT", nullable: true),
DefaultDockerImageIndex = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerImages", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ServerImageVariables",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Key = table.Column<string>(type: "TEXT", nullable: false),
DefaultValue = table.Column<string>(type: "TEXT", nullable: false),
DisplayName = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: false),
AllowUserToEdit = table.Column<bool>(type: "INTEGER", nullable: false),
AllowUserToView = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerImageVariables", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ServerNodes",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
Fqdn = table.Column<string>(type: "TEXT", nullable: false),
UseSsl = table.Column<bool>(type: "INTEGER", nullable: false),
Token = table.Column<string>(type: "TEXT", nullable: false),
HttpPort = table.Column<int>(type: "INTEGER", nullable: false),
FtpPort = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerNodes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ServerDockerImages",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
DisplayName = table.Column<string>(type: "TEXT", nullable: false),
AutoPull = table.Column<bool>(type: "INTEGER", nullable: false),
ServerImageId = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
IpAddress = table.Column<string>(type: "TEXT", nullable: false),
Port = table.Column<int>(type: "INTEGER", nullable: false),
ServerId = table.Column<int>(type: "INTEGER", nullable: true),
ServerNodeId = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ServiceId = table.Column<int>(type: "INTEGER", nullable: false),
Cpu = table.Column<int>(type: "INTEGER", nullable: false),
Memory = table.Column<int>(type: "INTEGER", nullable: false),
Disk = table.Column<int>(type: "INTEGER", nullable: false),
ImageId = table.Column<int>(type: "INTEGER", nullable: false),
DockerImageIndex = table.Column<int>(type: "INTEGER", nullable: false),
OverrideStartupCommand = table.Column<string>(type: "TEXT", nullable: true),
NodeId = table.Column<int>(type: "INTEGER", nullable: false),
MainAllocationId = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Key = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: false),
ServerId = table.Column<int>(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");
}
/// <inheritdoc />
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");
}
}
}

View file

@ -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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Avatar")
.HasColumnType("TEXT");
b.Property<double>("Balance")
.HasColumnType("REAL");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Flags")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Permissions")
.HasColumnType("INTEGER");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("TEXT");
b.Property<string>("TotpKey")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b =>
{
b.Property<int>("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<int>("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<int>("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<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Cpu")
.HasColumnType("INTEGER");
b.Property<int>("Disk")
.HasColumnType("INTEGER");
b.Property<int>("DockerImageIndex")
.HasColumnType("INTEGER");
b.Property<int>("ImageId")
.HasColumnType("INTEGER");
b.Property<int>("MainAllocationId")
.HasColumnType("INTEGER");
b.Property<int>("Memory")
.HasColumnType("INTEGER");
b.Property<int>("NodeId")
.HasColumnType("INTEGER");
b.Property<string>("OverrideStartupCommand")
.HasColumnType("TEXT");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("IpAddress")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Port")
.HasColumnType("INTEGER");
b.Property<int?>("ServerId")
.HasColumnType("INTEGER");
b.Property<int?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AutoPull")
.HasColumnType("INTEGER");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("ServerImageId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ServerImageId");
b.ToTable("ServerDockerImages");
});
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AllocationsNeeded")
.HasColumnType("INTEGER");
b.Property<string>("Author")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("DefaultDockerImageIndex")
.HasColumnType("INTEGER");
b.Property<string>("DonateUrl")
.HasColumnType("TEXT");
b.Property<string>("InstallDockerImage")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("InstallScript")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("InstallShell")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("OnlineDetection")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ParseConfigurations")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("StartupCommand")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("StopCommand")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("UpdateUrl")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ServerImages");
});
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AllowUserToEdit")
.HasColumnType("INTEGER");
b.Property<bool>("AllowUserToView")
.HasColumnType("INTEGER");
b.Property<string>("DefaultValue")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ServerImageVariables");
});
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Fqdn")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("FtpPort")
.HasColumnType("INTEGER");
b.Property<int>("HttpPort")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("UseSsl")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("ServerNodes");
});
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("ServerId")
.HasColumnType("INTEGER");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerVariables");
});
modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConfigJsonOverride")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasColumnType("TEXT");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER");
b.Property<int>("ProductId")
.HasColumnType("INTEGER");
b.Property<DateTime>("RenewAt")
.HasColumnType("TEXT");
b.Property<bool>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ServiceId")
.HasColumnType("INTEGER");
b.Property<int>("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<int>("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<int>("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<int>("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<int>("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<int>("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<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConfigJsonOverride")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasColumnType("TEXT");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER");
b.Property<int>("ProductId")
.HasColumnType("INTEGER");
b.Property<DateTime>("RenewAt")
.HasColumnType("TEXT");
b.Property<bool>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ServiceId")
.HasColumnType("INTEGER");
b.Property<int>("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<int>("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<int>("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<int>("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<int>("Id")
.ValueGeneratedOnAdd()
@ -469,52 +762,7 @@ namespace Moonlight.Core.Database.Migrations
b.ToTable("TicketMessages");
});
modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Avatar")
.HasColumnType("TEXT");
b.Property<double>("Balance")
.HasColumnType("REAL");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Flags")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Permissions")
.HasColumnType("INTEGER");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("TEXT");
b.Property<string>("TotpKey")
.HasColumnType("TEXT");
b.Property<string>("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
}
}

View file

@ -0,0 +1,117 @@
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
namespace Moonlight.Core.Helpers;
public class HttpApiClient<TException> : 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<string> 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<string> GetAsString(string path) =>
await Send(HttpMethod.Get, path);
public async Task<T> Get<T>(string path) =>
JsonConvert.DeserializeObject<T>(await Send(HttpMethod.Get, path))!;
#endregion
#region POST
public async Task<string> PostAsString(string path, string body, string contentType = "text/plain") =>
await Send(HttpMethod.Post, path, body, contentType);
public async Task<T> Post<T>(string path, object body) =>
JsonConvert.DeserializeObject<T>(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<string> PutAsString(string path, string body, string contentType = "text/plain") =>
await Send(HttpMethod.Put, path, body, contentType);
public async Task<T> Put<T>(string path, object body) =>
JsonConvert.DeserializeObject<T>(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<string> PatchAsString(string path, string body, string contentType = "text/plain") =>
await Send(HttpMethod.Patch, path, body, contentType);
public async Task<T> Patch<T>(string path, object body) =>
JsonConvert.DeserializeObject<T>(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<string> DeleteAsString(string path) =>
await Send(HttpMethod.Delete, path);
public async Task<T> Delete<T>(string path) =>
JsonConvert.DeserializeObject<T>(await Send(HttpMethod.Delete, path))!;
#endregion
public void Dispose()
{
Client.Dispose();
}
}

View file

@ -79,10 +79,35 @@ public class WsPacketConnection
return typedPacketType.GetProperty("Data")!.GetValue(typedPacket);
}
public async Task<T?> Receive<T>() 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

View file

@ -11,6 +11,7 @@ public enum Permission
AdminTickets = 1004,
AdminCommunity = 1030,
AdminServices = 1050,
AdminServers = 1060,
AdminStore = 1900,
AdminViewExceptions = 1999,
AdminRoot = 2000

View file

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

View file

@ -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<Repository<Server>>();
var imageRepo = provider.GetRequiredService<Repository<ServerImage>>();
var nodeRepo = provider.GetRequiredService<Repository<ServerNode>>();
var allocationRepo = provider.GetRequiredService<Repository<ServerAllocation>>();
var serverService = provider.GetRequiredService<ServerService>();
// Parse the configuration file
var config =
JsonConvert.DeserializeObject<ServerConfig>(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<ServerAllocation>();
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<Repository<Server>>();
var serverService = provider.GetRequiredService<ServerService>();
var serverVariableRepo = provider.GetRequiredService<Repository<ServerVariable>>();
// 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);
}
}

View file

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

View file

@ -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<UserLayout>();
}
public override Task BuildAdminView(ServiceViewContext context)
{
throw new NotImplementedException();
}
}

View file

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

View file

@ -20,5 +20,7 @@ public class ServerImage
public string? UpdateUrl { get; set; }
public List<ServerImageVariable> Variables = new();
public int DefaultDockerImageIndex { get; set; } = 0;
public List<ServerDockerImage> DockerImages { get; set; }
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,27 @@
namespace Moonlight.Features.Servers.Helpers;
public class MetaCache<T>
{
private readonly Dictionary<int, T> Cache = new();
public Task Update(int id, Action<T> metaAction)
{
lock (Cache)
{
T? meta = default;
if (Cache.ContainsKey(id))
meta = Cache[id];
if (meta == null)
{
meta = Activator.CreateInstance<T>();
Cache.Add(id, meta);
}
metaAction.Invoke(meta);
}
return Task.CompletedTask;
}
}

View file

@ -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<ActionResult> 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>("serverStateUpdate");
await wsPacketConnection.RegisterPacket<ServerOutputMessage>("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();
}
}

View file

@ -25,7 +25,7 @@ public class ServersControllers : Controller
public async Task<ActionResult> 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<int>("amount");
await wsPacketConnection.RegisterPacket<ServerConfiguration>("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<ActionResult<ServerConfiguration>> 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<ActionResult<ServerInstallConfiguration>> 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);
}
}

View file

@ -8,12 +8,12 @@ namespace Moonlight.Features.Servers.Http.Middleware;
public class NodeMiddleware
{
private RequestDelegate Next;
private readonly Repository<ServerNode> NodeRepository;
private readonly IServiceProvider ServiceProvider;
public NodeMiddleware(RequestDelegate next, Repository<ServerNode> 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<Repository<ServerNode>>();
// Check if any node has the token specified by the request
var node = NodeRepository
var node = nodeRepo
.Get()
.FirstOrDefault(x => x.Token == token);

View file

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

View file

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

View file

@ -0,0 +1,9 @@
namespace Moonlight.Features.Servers.Models.Enums;
public enum PowerAction
{
Start,
Stop,
Kill,
Install
}

View file

@ -0,0 +1,11 @@
namespace Moonlight.Features.Servers.Models.Enums;
public enum ServerState
{
Offline,
Starting,
Online,
Stopping,
Installing,
Join2Start
}

View file

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

View file

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

View file

@ -0,0 +1,7 @@
namespace Moonlight.Features.Servers.Models.Packets;
public class ServerOutputMessage
{
public int Id { get; set; }
public string Message { get; set; }
}

View file

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

View file

@ -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<int, NodeMeta> MetaCache = new();
public Task UpdateMeta(ServerNode node, Action<NodeMeta> 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<NodeMeta> Meta = new();
}

View file

@ -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<ServerMeta> 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<NodeException> CreateHttpClient(Server server)
{
using var scope = ServiceProvider.CreateScope();
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
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<NodeException>(remoteUrl, serverWithNode.Node.Token);
}
}

View file

@ -0,0 +1,21 @@
<div class="card mb-5 mb-xl-10">
<div class="card-body pt-0 pb-0">
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/servers">
<i class="bx bx-sm bx-server me-2"></i> Nodes
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/servers/images">
<i class="bx bx-sm bx-memory-card me-2"></i> Images
</a>
</li>
</ul>
</div>
</div>
@code
{
[Parameter] public int Index { get; set; }
}

View file

@ -0,0 +1,85 @@
@using XtermBlazor
@using Moonlight.Core.Services.Interop
@inject ClipboardService ClipboardService
@inject ToastService ToastService
<Xterm @ref="Term" Options="Options" AddonIds="AddonIds" OnFirstRender="OnFirstRender" />
@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<string> 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;
}
}

View file

@ -0,0 +1,5 @@
<h3>AdminLayout</h3>
@code {
}

View file

@ -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<Server> ServerRepository
@inject ServerService ServerService
@inject ToastService ToastService
@implements IDisposable
<LazyLoader Load="Load" ShowAsCard="true">
<div class="card card-body pb-0 pt-5">
<div class="d-flex justify-content-between">
<div class="d-flex flex-row">
<div class="d-flex flex-column ms-3">
<span class="fw-bold text-gray-900 fs-3">
@(Service.Nickname ?? $"Service {Service.Id}")
</span>
<span class="text-gray-500 pt-2 fw-semibold fs-6">
@(Server.Image.Name)
</span>
</div>
<div class="vr mx-4"></div>
<div class="d-flex flex-column">
<div class="text-gray-900 fs-4">
@{
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;
}
}
<i class="bx bx-sm bxs-circle text-@(color) @(Meta.State != ServerState.Offline ? $"pulse pulse-{color}" : "") align-middle"></i>
<span class="align-middle">
@(Meta.State)
<span class="text-muted">(@(Formatter.FormatUptime(DateTime.UtcNow - Meta.LastChangeTimestamp)))</span>
</span>
</div>
<div class="text-gray-800 pt-3 fw-semibold fs-5 row">
<div class="col-auto">
<span>
<i class="bx bx-sm bx-globe align-middle text-info"></i>
<span class="align-middle">@(Server.Node.Fqdn):@(Server.MainAllocation.Port)</span>
</span>
</div>
<div class="col-auto">
<span>
<i class="bx bx-sm bx-globe align-middle text-info"></i>
<span class="align-middle">188.75.252.37:10324</span>
</span>
</div>
</div>
</div>
</div>
<div>
<div class="mt-2">
@if (Meta.State == ServerState.Offline)
{
<WButton
OnClick="Start"
CssClasses="btn btn-light-success btn-icon me-1 my-1">
<i class="bx bx-sm bx-play"></i>
</WButton>
}
else
{
<button type="button" class="btn btn-light-success btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-play"></i>
</button>
}
@if (Meta.State == ServerState.Offline || Meta.State == ServerState.Installing)
{
<button class="btn btn-light-warning btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-power-off"></i>
</button>
}
else
{
<WButton
OnClick="Stop"
CssClasses="btn btn-light-warning btn-icon me-1 my-1">
<i class="bx bx-sm bx-power-off"></i>
</WButton>
}
@if (Meta.State == ServerState.Offline || Meta.State == ServerState.Installing)
{
<button class="btn btn-light-danger btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-bomb"></i>
</button>
}
else
{
<WButton
OnClick="Kill"
CssClasses="btn btn-light-danger btn-icon me-1 my-1">
<i class="bx bx-sm bx-bomb"></i>
</WButton>
}
</div>
</div>
</div>
<ul class="ms-2 mt-5 nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-6 fw-bold text-nowrap flex-nowrap hide-scrollbar" style="overflow-x: auto; overflow-y: hidden">
@{
var routeWithSlash = "/" + (Route ?? "");
}
@foreach (var uiPage in ViewContext.Pages)
{
<li class="nav-item">
<a class="nav-link text-active-primary ms-0 me-10v @(routeWithSlash == uiPage.Route ? "active" : "")" href="/service/@($"{Service.Id}{uiPage.Route}")">
<i class="@(uiPage.Icon) me-2"></i>
@(uiPage.Name)
</a>
</li>
}
</ul>
</div>
<div class="mt-5">
@if (IsInstalling)
{
<div class="card card-body bg-black p-3">
<Terminal @ref="InstallTerminal"/>
</div>
}
else
{
<CascadingValue Value="Server">
<CascadingValue Value="Service">
<CascadingValue Value="Meta">
<SmartRouter Route="@Route">
@foreach (var uiPage in ViewContext.Pages)
{
<Route Path="@uiPage.Route">
@uiPage.Component
</Route>
}
</SmartRouter>
</CascadingValue>
</CascadingValue>
</CascadingValue>
}
</div>
</LazyLoader>
@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();
}
}

View file

@ -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<ServerNode> NodeRepository
<AdminServersNavigation Index="0"/>
<AutoCrud TItem="ServerNode"
TCreateForm="CreateNodeForm"
TUpdateForm="UpdateNodeForm"
Title="Manage nodes"
Load="Load"
ValidateAdd="ValidateAdd"
ValidateDelete="ValidateDelete">
<View>
<Column TableItem="ServerNode" Field="@(x => x.Id)" Title="Id" />
<Column TableItem="ServerNode" Field="@(x => x.Name)" Title="Name">
<Template>
<a href="/admin/servers/nodes/@(context.Id)">@(context.Name)</a>
</Template>
</Column>
<Column TableItem="ServerNode" Field="@(x => x.Fqdn)" Title="Fqdn" />
<Column TableItem="ServerNode" Field="@(x => x.Id)" Title="Status">
<Template>
<span class="text-success">Online</span>
</Template>
</Column>
</View>
</AutoCrud>
@code
{
private ServerNode[] Load(Repository<ServerNode> 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;
}
}

View file

@ -0,0 +1,7 @@
@page "/admin/servers/nodes/{Id:int}"
@code
{
[Parameter]
public int Id { get; set; }
}

View file

@ -34,13 +34,8 @@
<Folder Include="Features\Dummy\UI\Components\" />
<Folder Include="Features\Dummy\UI\Views\" />
<Folder Include="Features\Servers\Configuration\" />
<Folder Include="Features\Servers\Exceptions\" />
<Folder Include="Features\Servers\Helpers\" />
<Folder Include="Features\Servers\Http\Requests\" />
<Folder Include="Features\Servers\Http\Resources\" />
<Folder Include="Features\Servers\Models\Forms\" />
<Folder Include="Features\Servers\UI\Components\" />
<Folder Include="Features\Servers\UI\Views\" />
<Folder Include="Features\StoreSystem\Helpers\" />
<Folder Include="Features\Ticketing\Models\Abstractions\" />
</ItemGroup>
@ -68,6 +63,7 @@
<PackageReference Include="Serilog" Version="3.1.0-dev-02078" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0-dev-00923" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00972" />
<PackageReference Include="XtermBlazor" Version="1.10.0" />
</ItemGroup>
<ItemGroup>

View file

@ -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<ModalService>();
builder.Services.AddScoped<AlertService>();
builder.Services.AddScoped<FileDownloadService>();
builder.Services.AddScoped<AdBlockService>();
builder.Services.AddScoped<ClipboardService>();
// Services / Store
builder.Services.AddScoped<StoreService>();
@ -107,6 +110,7 @@ builder.Services.AddScoped<TicketCreateService>();
// Services / Servers
builder.Services.AddSingleton<NodeService>();
builder.Services.AddSingleton<ServerService>();
// Services
builder.Services.AddScoped<IdentityService>();
@ -135,6 +139,7 @@ var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseWebSockets();
app.UseMiddleware<NodeMiddleware>();
@ -150,7 +155,7 @@ moonlightService.Application = app;
moonlightService.LogPath = logPath;
var serviceService = app.Services.GetRequiredService<ServiceDefinitionService>();
serviceService.Register<DummyServiceDefinition>(ServiceType.Server);
serviceService.Register<ServerServiceDefinition>(ServiceType.Server);
await pluginService.RunPrePost(app);

View file

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