29 Commits

Author SHA1 Message Date
f6b71f4de6 Upgraded mooncore. Done required refactoring to function with new version 2025-10-20 19:27:31 +00:00
85392208c4 Updated to latest moonlight and mooncore version. Done refactoring to async scheme and other changes. Recreated database migrations and cleaned models 2025-09-22 12:13:57 +02:00
91fb15a03e Started implementing server service and daemon controllers 2025-09-15 21:47:07 +02:00
32f447d268 Deed debug handler. Added installation handler. Improved docker console streaming 2025-09-13 20:53:03 +02:00
160446eed0 Added online detection handler 2025-09-09 23:08:01 +02:00
b90100d250 Implemented restorer, runtime and dummy statistics. Added service registering and fixed server factory. Moved logger to server context 2025-09-07 23:15:48 +02:00
282096595d Improved comments. Started implementing docker components and other base components. Updated dependencies 2025-09-06 21:44:22 +02:00
348e9560ab Cleaned up interfaces. Extracted server state machine trigger handler to seperated classes. Removed legacy code 2025-09-06 15:34:35 +02:00
7587a7e8e3 Cleaned up project files from legacy plugin settings 2025-08-24 11:51:09 +02:00
7c2bc9d19b Improved logging in server components 2025-08-02 21:54:58 +02:00
2e4c933fbe Started implementing server deletion 2025-08-02 21:12:38 +02:00
5c170935b4 Implemented online detection. Extended ServerContext to include self reference so sub components can subscribe to the state. Improved console module detach handling. Implemented new server service to replace the old one. Added log restore when restoring 2025-07-30 20:52:24 +02:00
eaf8c36f7f Fixed event/observer issues 2025-07-30 17:12:21 +02:00
bb81ca9674 Implemented first iteration of the docker-based server installer. Added restore functionality for the installer. Wired up for basic installer testing 2025-07-29 22:24:46 +02:00
f57d33cb1e Fixed usage of IAsyncObservable. Added runtime exit handler 2025-07-29 21:14:41 +02:00
b546a168d2 Implemented restorer, wired up for basic testing. Improved abstractions and fixed observer pattern issues 2025-07-26 23:19:57 +02:00
84b3d1caf6 Implemented factory pattern for server abstraction creation. Implemented raw fs and docker provisioner. Implemented docker event service with observer pattern 2025-07-26 19:14:02 +02:00
0bef60dbc8 For extensions of base system like podman and btrfs: Started improving server abstractions to make it more extendable in order to support multiple implementations 2025-07-25 13:45:47 +02:00
bdc4ad8265 Added id ordering as postgres would return the last changed elements first which breaks pagination 2025-07-24 20:24:00 +02:00
431cdcb260 Improved server share permission handling and share ui 2025-07-24 20:19:49 +02:00
1f94752c54 Started improving server shares and general api controller structure 2025-07-24 18:28:10 +02:00
a2db7be26f Improved server header and variables page 2025-07-18 23:17:56 +02:00
265a4b280b Refactored ui. Improved console experience. Added command endpoint 2025-07-18 21:16:52 +02:00
f8c11b2dd8 Updated mooncore version 2025-07-17 22:56:52 +02:00
e83d1351cb Fixed server create ui 2025-07-16 21:29:43 +02:00
61253919cf Refactored frontend to work with the latest mooncore changes 2025-07-16 20:46:45 +02:00
383d4bb24b Improved some route templates 2025-07-15 21:06:40 +02:00
f22f0c0e51 Refactored api server project for latest mooncore changes 2025-07-15 21:04:46 +02:00
514f862a9d Started refactoring to be compatible with the changed nuget packages and the mooncore changes 2025-07-15 19:21:44 +02:00
248 changed files with 10672 additions and 7461 deletions

View File

@@ -0,0 +1,10 @@
using MoonCore.PluginFramework;
using Moonlight.ApiServer.Plugins;
namespace MoonlightServers.ApiServer.Runtime;
[PluginLoader]
public partial class DevPluginLoader : IPluginStartup
{
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MoonlightServers.ApiServer\MoonlightServers.ApiServer.csproj"/>
<ProjectReference Include="..\MoonlightServers.Frontend.Runtime\MoonlightServers.Frontend.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.9" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
</ItemGroup>
<ItemGroup>
<Content Update="Properties\launchSettings.json">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,34 @@
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Startup;
using MoonlightServers.ApiServer.Runtime;
var pluginLoader = new DevPluginLoader();
pluginLoader.Initialize();
var builder = WebApplication.CreateBuilder(args);
builder.AddMoonlight(pluginLoader.Instances);
var app = builder.Build();
app.UseMoonlight(pluginLoader.Instances);
// Add frontend
var configuration = AppConfiguration.CreateEmpty();
builder.Configuration.Bind(configuration);
// Handle setup of wasm app hosting in the runtime
// so the Moonlight.ApiServer doesn't need the wasm package
if (configuration.Frontend.EnableHosting)
{
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
}
app.MapMoonlight(pluginLoader.Instances);
await app.RunAsync();

View File

@@ -18,8 +18,4 @@ public class Node
public int HttpPort { get; set; }
public int FtpPort { get; set; }
public bool UseSsl { get; set; }
// Misc
public bool EnableTransparentMode { get; set; }
public bool EnableDynamicFirewall { get; set; }
}

View File

@@ -24,6 +24,4 @@ public class Server
public int Cpu { get; set; }
public int Memory { get; set; }
public int Disk { get; set; }
public bool UseVirtualDisk { get; set; }
public int Bandwidth { get; set; }
}

View File

@@ -1,452 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
[DbContext(typeof(ServersDataContext))]
[Migration("20250226210232_RecreatedMigrationsForPostgresql")]
partial class RecreatedMigrationsForPostgresql
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("IpAddress")
.IsRequired()
.HasColumnType("text");
b.Property<int>("NodeId")
.HasColumnType("integer");
b.Property<int>("Port")
.HasColumnType("integer");
b.Property<int?>("ServerId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("ServerId");
b.ToTable("Servers_Allocations", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("EnableDynamicFirewall")
.HasColumnType("boolean");
b.Property<bool>("EnableTransparentMode")
.HasColumnType("boolean");
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("boolean");
b.HasKey("Id");
b.ToTable("Servers_Nodes", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Bandwidth")
.HasColumnType("integer");
b.Property<int>("Cpu")
.HasColumnType("integer");
b.Property<int>("Disk")
.HasColumnType("integer");
b.Property<int>("DockerImageIndex")
.HasColumnType("integer");
b.Property<int>("Memory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("NodeId")
.HasColumnType("integer");
b.Property<int>("OwnerId")
.HasColumnType("integer");
b.Property<int>("StarId")
.HasColumnType("integer");
b.Property<string>("StartupOverride")
.HasColumnType("text");
b.Property<bool>("UseVirtualDisk")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers_Servers", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("Completed")
.HasColumnType("boolean");
b.Property<DateTime>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("ServerId")
.HasColumnType("integer");
b.Property<long>("Size")
.HasColumnType("bigint");
b.Property<bool>("Successful")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("Servers_ServerBackups", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
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("Servers_ServerVariables", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowDockerImageChange")
.HasColumnType("boolean");
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<int>("DefaultDockerImage")
.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>("ParseConfiguration")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RequiredAllocations")
.HasColumnType("integer");
b.Property<string>("StartupCommand")
.IsRequired()
.HasColumnType("text");
b.Property<string>("StopCommand")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UpdateUrl")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Servers_Stars", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AutoPulling")
.HasColumnType("boolean");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StarId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StarId");
b.ToTable("Servers_StarDockerImages", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowEditing")
.HasColumnType("boolean");
b.Property<bool>("AllowViewing")
.HasColumnType("boolean");
b.Property<string>("DefaultValue")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Filter")
.HasColumnType("text");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StarId")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StarId");
b.ToTable("Servers_StarVariables", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node")
.WithMany("Allocations")
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Allocations")
.HasForeignKey("ServerId");
b.Navigation("Node");
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node")
.WithMany("Servers")
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany()
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Node");
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", null)
.WithMany("Backups")
.HasForeignKey("ServerId");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Variables")
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany("DockerImages")
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany("Variables")
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Navigation("Allocations");
b.Navigation("Servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Navigation("Allocations");
b.Navigation("Backups");
b.Navigation("Variables");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,456 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
[DbContext(typeof(ServersDataContext))]
[Migration("20250301142415_AddedTokenIdField")]
partial class AddedTokenIdField
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("IpAddress")
.IsRequired()
.HasColumnType("text");
b.Property<int>("NodeId")
.HasColumnType("integer");
b.Property<int>("Port")
.HasColumnType("integer");
b.Property<int?>("ServerId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("ServerId");
b.ToTable("Servers_Allocations", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("EnableDynamicFirewall")
.HasColumnType("boolean");
b.Property<bool>("EnableTransparentMode")
.HasColumnType("boolean");
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<string>("TokenId")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("UseSsl")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("Servers_Nodes", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Bandwidth")
.HasColumnType("integer");
b.Property<int>("Cpu")
.HasColumnType("integer");
b.Property<int>("Disk")
.HasColumnType("integer");
b.Property<int>("DockerImageIndex")
.HasColumnType("integer");
b.Property<int>("Memory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("NodeId")
.HasColumnType("integer");
b.Property<int>("OwnerId")
.HasColumnType("integer");
b.Property<int>("StarId")
.HasColumnType("integer");
b.Property<string>("StartupOverride")
.HasColumnType("text");
b.Property<bool>("UseVirtualDisk")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers_Servers", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("Completed")
.HasColumnType("boolean");
b.Property<DateTime>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("ServerId")
.HasColumnType("integer");
b.Property<long>("Size")
.HasColumnType("bigint");
b.Property<bool>("Successful")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("Servers_ServerBackups", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
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("Servers_ServerVariables", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowDockerImageChange")
.HasColumnType("boolean");
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<int>("DefaultDockerImage")
.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>("ParseConfiguration")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RequiredAllocations")
.HasColumnType("integer");
b.Property<string>("StartupCommand")
.IsRequired()
.HasColumnType("text");
b.Property<string>("StopCommand")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UpdateUrl")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Servers_Stars", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AutoPulling")
.HasColumnType("boolean");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StarId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StarId");
b.ToTable("Servers_StarDockerImages", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowEditing")
.HasColumnType("boolean");
b.Property<bool>("AllowViewing")
.HasColumnType("boolean");
b.Property<string>("DefaultValue")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Filter")
.HasColumnType("text");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StarId")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StarId");
b.ToTable("Servers_StarVariables", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node")
.WithMany("Allocations")
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Allocations")
.HasForeignKey("ServerId");
b.Navigation("Node");
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node")
.WithMany("Servers")
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany()
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Node");
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", null)
.WithMany("Backups")
.HasForeignKey("ServerId");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Variables")
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany("DockerImages")
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany("Variables")
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Navigation("Allocations");
b.Navigation("Servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Navigation("Allocations");
b.Navigation("Backups");
b.Navigation("Variables");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedTokenIdField : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "TokenId",
table: "Servers_Nodes",
type: "text",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TokenId",
table: "Servers_Nodes");
}
}
}

View File

@@ -1,51 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedShares : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Servers_ServerShares",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<int>(type: "integer", nullable: false),
ServerId = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Content = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_ServerShares", x => x.Id);
table.ForeignKey(
name: "FK_Servers_ServerShares_Servers_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers_Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Servers_ServerShares_ServerId",
table: "Servers_ServerShares",
column: "ServerId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Servers_ServerShares");
}
}
}

View File

@@ -12,15 +12,16 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace MoonlightServers.ApiServer.Database.Migrations
{
[DbContext(typeof(ServersDataContext))]
[Migration("20250606121013_AddedShares")]
partial class AddedShares
[Migration("20250922091731_RecreatedModelsInNewSchema")]
partial class RecreatedModelsInNewSchema
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -52,7 +53,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("ServerId");
b.ToTable("Servers_Allocations", (string)null);
b.ToTable("Allocations", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
@@ -63,12 +64,6 @@ namespace MoonlightServers.ApiServer.Database.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("EnableDynamicFirewall")
.HasColumnType("boolean");
b.Property<bool>("EnableTransparentMode")
.HasColumnType("boolean");
b.Property<string>("Fqdn")
.IsRequired()
.HasColumnType("text");
@@ -96,7 +91,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasKey("Id");
b.ToTable("Servers_Nodes", (string)null);
b.ToTable("Nodes", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
@@ -107,9 +102,6 @@ namespace MoonlightServers.ApiServer.Database.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Bandwidth")
.HasColumnType("integer");
b.Property<int>("Cpu")
.HasColumnType("integer");
@@ -138,16 +130,13 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.Property<string>("StartupOverride")
.HasColumnType("text");
b.Property<bool>("UseVirtualDisk")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers_Servers", (string)null);
b.ToTable("Servers", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
@@ -180,7 +169,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("ServerId");
b.ToTable("Servers_ServerBackups", (string)null);
b.ToTable("ServerBackups", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerShare", b =>
@@ -207,7 +196,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("ServerId");
b.ToTable("Servers_ServerShares", (string)null);
b.ToTable("ServerShares", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
@@ -233,7 +222,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("ServerId");
b.ToTable("Servers_ServerVariables", (string)null);
b.ToTable("ServerVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
@@ -301,7 +290,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasKey("Id");
b.ToTable("Servers_Stars", (string)null);
b.ToTable("Stars", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
@@ -330,7 +319,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("StarId");
b.ToTable("Servers_StarDockerImages", (string)null);
b.ToTable("StarDockerImages", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
@@ -376,7 +365,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("StarId");
b.ToTable("Servers_StarVariables", (string)null);
b.ToTable("StarVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
@@ -437,14 +426,14 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b1.HasKey("ServerShareId");
b1.ToTable("Servers_ServerShares");
b1.ToTable("ServerShares", "servers");
b1.ToJson("Content");
b1.WithOwner()
.HasForeignKey("ServerShareId");
b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerSharePermission", "Permissions", b2 =>
b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerShareContent+SharePermission", "Permissions", b2 =>
{
b2.Property<int>("ServerShareContentServerShareId")
.HasColumnType("integer");
@@ -453,16 +442,16 @@ namespace MoonlightServers.ApiServer.Database.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("integer");
b2.Property<string>("Name")
b2.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b2.Property<int>("Type")
b2.Property<int>("Level")
.HasColumnType("integer");
b2.HasKey("ServerShareContentServerShareId", "__synthesizedOrdinal");
b2.ToTable("Servers_ServerShares");
b2.ToTable("ServerShares", "servers");
b2.WithOwner()
.HasForeignKey("ServerShareContentServerShareId");

View File

@@ -7,13 +7,17 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace MoonlightServers.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class RecreatedMigrationsForPostgresql : Migration
public partial class RecreatedModelsInNewSchema : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "servers");
migrationBuilder.CreateTable(
name: "Servers_Nodes",
name: "Nodes",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
@@ -21,19 +25,19 @@ namespace MoonlightServers.ApiServer.Database.Migrations
Name = table.Column<string>(type: "text", nullable: false),
Fqdn = table.Column<string>(type: "text", nullable: false),
Token = table.Column<string>(type: "text", nullable: false),
TokenId = table.Column<string>(type: "text", nullable: false),
HttpPort = table.Column<int>(type: "integer", nullable: false),
FtpPort = table.Column<int>(type: "integer", nullable: false),
UseSsl = table.Column<bool>(type: "boolean", nullable: false),
EnableTransparentMode = table.Column<bool>(type: "boolean", nullable: false),
EnableDynamicFirewall = table.Column<bool>(type: "boolean", nullable: false)
UseSsl = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_Nodes", x => x.Id);
table.PrimaryKey("PK_Nodes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Servers_Stars",
name: "Stars",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
@@ -56,11 +60,12 @@ namespace MoonlightServers.ApiServer.Database.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_Stars", x => x.Id);
table.PrimaryKey("PK_Stars", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Servers_Servers",
name: "Servers",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
@@ -73,29 +78,30 @@ namespace MoonlightServers.ApiServer.Database.Migrations
DockerImageIndex = 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),
UseVirtualDisk = table.Column<bool>(type: "boolean", nullable: false),
Bandwidth = table.Column<int>(type: "integer", nullable: false)
Disk = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_Servers", x => x.Id);
table.PrimaryKey("PK_Servers", x => x.Id);
table.ForeignKey(
name: "FK_Servers_Servers_Servers_Nodes_NodeId",
name: "FK_Servers_Nodes_NodeId",
column: x => x.NodeId,
principalTable: "Servers_Nodes",
principalSchema: "servers",
principalTable: "Nodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Servers_Servers_Servers_Stars_StarId",
name: "FK_Servers_Stars_StarId",
column: x => x.StarId,
principalTable: "Servers_Stars",
principalSchema: "servers",
principalTable: "Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Servers_StarDockerImages",
name: "StarDockerImages",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
@@ -107,17 +113,19 @@ namespace MoonlightServers.ApiServer.Database.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_StarDockerImages", x => x.Id);
table.PrimaryKey("PK_StarDockerImages", x => x.Id);
table.ForeignKey(
name: "FK_Servers_StarDockerImages_Servers_Stars_StarId",
name: "FK_StarDockerImages_Stars_StarId",
column: x => x.StarId,
principalTable: "Servers_Stars",
principalSchema: "servers",
principalTable: "Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Servers_StarVariables",
name: "StarVariables",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
@@ -134,17 +142,19 @@ namespace MoonlightServers.ApiServer.Database.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_StarVariables", x => x.Id);
table.PrimaryKey("PK_StarVariables", x => x.Id);
table.ForeignKey(
name: "FK_Servers_StarVariables_Servers_Stars_StarId",
name: "FK_StarVariables_Stars_StarId",
column: x => x.StarId,
principalTable: "Servers_Stars",
principalSchema: "servers",
principalTable: "Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Servers_Allocations",
name: "Allocations",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
@@ -156,22 +166,25 @@ namespace MoonlightServers.ApiServer.Database.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_Allocations", x => x.Id);
table.PrimaryKey("PK_Allocations", x => x.Id);
table.ForeignKey(
name: "FK_Servers_Allocations_Servers_Nodes_NodeId",
name: "FK_Allocations_Nodes_NodeId",
column: x => x.NodeId,
principalTable: "Servers_Nodes",
principalSchema: "servers",
principalTable: "Nodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Servers_Allocations_Servers_Servers_ServerId",
name: "FK_Allocations_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers_Servers",
principalSchema: "servers",
principalTable: "Servers",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Servers_ServerBackups",
name: "ServerBackups",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
@@ -185,16 +198,43 @@ namespace MoonlightServers.ApiServer.Database.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_ServerBackups", x => x.Id);
table.PrimaryKey("PK_ServerBackups", x => x.Id);
table.ForeignKey(
name: "FK_Servers_ServerBackups_Servers_Servers_ServerId",
name: "FK_ServerBackups_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers_Servers",
principalSchema: "servers",
principalTable: "Servers",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Servers_ServerVariables",
name: "ServerShares",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<int>(type: "integer", nullable: false),
ServerId = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Content = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerShares", x => x.Id);
table.ForeignKey(
name: "FK_ServerShares_Servers_ServerId",
column: x => x.ServerId,
principalSchema: "servers",
principalTable: "Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ServerVariables",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
@@ -205,53 +245,68 @@ namespace MoonlightServers.ApiServer.Database.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_ServerVariables", x => x.Id);
table.PrimaryKey("PK_ServerVariables", x => x.Id);
table.ForeignKey(
name: "FK_Servers_ServerVariables_Servers_Servers_ServerId",
name: "FK_ServerVariables_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers_Servers",
principalSchema: "servers",
principalTable: "Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Servers_Allocations_NodeId",
table: "Servers_Allocations",
name: "IX_Allocations_NodeId",
schema: "servers",
table: "Allocations",
column: "NodeId");
migrationBuilder.CreateIndex(
name: "IX_Servers_Allocations_ServerId",
table: "Servers_Allocations",
name: "IX_Allocations_ServerId",
schema: "servers",
table: "Allocations",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_Servers_ServerBackups_ServerId",
table: "Servers_ServerBackups",
name: "IX_ServerBackups_ServerId",
schema: "servers",
table: "ServerBackups",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_Servers_Servers_NodeId",
table: "Servers_Servers",
name: "IX_Servers_NodeId",
schema: "servers",
table: "Servers",
column: "NodeId");
migrationBuilder.CreateIndex(
name: "IX_Servers_Servers_StarId",
table: "Servers_Servers",
name: "IX_Servers_StarId",
schema: "servers",
table: "Servers",
column: "StarId");
migrationBuilder.CreateIndex(
name: "IX_Servers_ServerVariables_ServerId",
table: "Servers_ServerVariables",
name: "IX_ServerShares_ServerId",
schema: "servers",
table: "ServerShares",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_Servers_StarDockerImages_StarId",
table: "Servers_StarDockerImages",
name: "IX_ServerVariables_ServerId",
schema: "servers",
table: "ServerVariables",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_StarDockerImages_StarId",
schema: "servers",
table: "StarDockerImages",
column: "StarId");
migrationBuilder.CreateIndex(
name: "IX_Servers_StarVariables_StarId",
table: "Servers_StarVariables",
name: "IX_StarVariables_StarId",
schema: "servers",
table: "StarVariables",
column: "StarId");
}
@@ -259,28 +314,40 @@ namespace MoonlightServers.ApiServer.Database.Migrations
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Servers_Allocations");
name: "Allocations",
schema: "servers");
migrationBuilder.DropTable(
name: "Servers_ServerBackups");
name: "ServerBackups",
schema: "servers");
migrationBuilder.DropTable(
name: "Servers_ServerVariables");
name: "ServerShares",
schema: "servers");
migrationBuilder.DropTable(
name: "Servers_StarDockerImages");
name: "ServerVariables",
schema: "servers");
migrationBuilder.DropTable(
name: "Servers_StarVariables");
name: "StarDockerImages",
schema: "servers");
migrationBuilder.DropTable(
name: "Servers_Servers");
name: "StarVariables",
schema: "servers");
migrationBuilder.DropTable(
name: "Servers_Nodes");
name: "Servers",
schema: "servers");
migrationBuilder.DropTable(
name: "Servers_Stars");
name: "Nodes",
schema: "servers");
migrationBuilder.DropTable(
name: "Stars",
schema: "servers");
}
}
}

View File

@@ -17,7 +17,8 @@ namespace MoonlightServers.ApiServer.Database.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -49,7 +50,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("ServerId");
b.ToTable("Servers_Allocations", (string)null);
b.ToTable("Allocations", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
@@ -60,12 +61,6 @@ namespace MoonlightServers.ApiServer.Database.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("EnableDynamicFirewall")
.HasColumnType("boolean");
b.Property<bool>("EnableTransparentMode")
.HasColumnType("boolean");
b.Property<string>("Fqdn")
.IsRequired()
.HasColumnType("text");
@@ -93,7 +88,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasKey("Id");
b.ToTable("Servers_Nodes", (string)null);
b.ToTable("Nodes", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
@@ -104,9 +99,6 @@ namespace MoonlightServers.ApiServer.Database.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Bandwidth")
.HasColumnType("integer");
b.Property<int>("Cpu")
.HasColumnType("integer");
@@ -135,16 +127,13 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.Property<string>("StartupOverride")
.HasColumnType("text");
b.Property<bool>("UseVirtualDisk")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers_Servers", (string)null);
b.ToTable("Servers", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
@@ -177,7 +166,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("ServerId");
b.ToTable("Servers_ServerBackups", (string)null);
b.ToTable("ServerBackups", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerShare", b =>
@@ -204,7 +193,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("ServerId");
b.ToTable("Servers_ServerShares", (string)null);
b.ToTable("ServerShares", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
@@ -230,7 +219,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("ServerId");
b.ToTable("Servers_ServerVariables", (string)null);
b.ToTable("ServerVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
@@ -298,7 +287,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasKey("Id");
b.ToTable("Servers_Stars", (string)null);
b.ToTable("Stars", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
@@ -327,7 +316,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("StarId");
b.ToTable("Servers_StarDockerImages", (string)null);
b.ToTable("StarDockerImages", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
@@ -373,7 +362,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.HasIndex("StarId");
b.ToTable("Servers_StarVariables", (string)null);
b.ToTable("StarVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
@@ -434,14 +423,14 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b1.HasKey("ServerShareId");
b1.ToTable("Servers_ServerShares");
b1.ToTable("ServerShares", "servers");
b1.ToJson("Content");
b1.WithOwner()
.HasForeignKey("ServerShareId");
b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerSharePermission", "Permissions", b2 =>
b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerShareContent+SharePermission", "Permissions", b2 =>
{
b2.Property<int>("ServerShareContentServerShareId")
.HasColumnType("integer");
@@ -450,16 +439,16 @@ namespace MoonlightServers.ApiServer.Database.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("integer");
b2.Property<string>("Name")
b2.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b2.Property<int>("Type")
b2.Property<int>("Level")
.HasColumnType("integer");
b2.HasKey("ServerShareContentServerShareId", "__synthesizedOrdinal");
b2.ToTable("Servers_ServerShares");
b2.ToTable("ServerShares", "servers");
b2.WithOwner()
.HasForeignKey("ServerShareContentServerShareId");

View File

@@ -1,16 +1,12 @@
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.SingleDb;
using Moonlight.ApiServer.Configuration;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
namespace MoonlightServers.ApiServer.Database;
public class ServersDataContext : DatabaseContext
public class ServersDataContext : DbContext
{
public override string Prefix { get; } = "Servers";
public DbSet<Allocation> Allocations { get; set; }
public DbSet<Node> Nodes { get; set; }
public DbSet<Server> Servers { get; set; }
@@ -21,26 +17,43 @@ public class ServersDataContext : DatabaseContext
public DbSet<StarDockerImage> StarDockerImages { get; set; }
public DbSet<StarVariable> StarVariables { get; set; }
private readonly AppConfiguration Configuration;
private readonly string Schema = "servers";
public ServersDataContext(AppConfiguration configuration)
{
Options = new()
Configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
Host = configuration.Database.Host,
Port = configuration.Database.Port,
Username = configuration.Database.Username,
Password = configuration.Database.Password,
Database = configuration.Database.Database
};
if(optionsBuilder.IsConfigured)
return;
var database = Configuration.Database;
var connectionString = $"Host={database.Host};" +
$"Port={database.Port};" +
$"Database={database.Database};" +
$"Username={database.Username};" +
$"Password={database.Password}";
optionsBuilder.UseNpgsql(connectionString, builder =>
{
builder.MigrationsHistoryTable("MigrationsHistory", Schema);
});
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Model.SetDefaultSchema(Schema);
base.OnModelCreating(modelBuilder);
#region Shares
modelBuilder.Ignore<ServerShareContent>();
modelBuilder.Ignore<ServerSharePermission>();
modelBuilder.Ignore<ServerShareContent.SharePermission>();
modelBuilder.Entity<ServerShare>(builder =>
{

View File

@@ -1,5 +1,3 @@
using MoonlightServers.DaemonShared.Enums;
using MoonlightServers.Shared.Enums;
using ServerState = MoonlightServers.Shared.Enums.ServerState;
namespace MoonlightServers.ApiServer.Extensions;

View File

@@ -2,6 +2,8 @@ using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;

View File

@@ -1,18 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.NodeAllocations;
using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes;
[ApiController]
[Route("api/admin/servers/nodes")]
[Route("api/admin/servers/nodes/{nodeId:int}/allocations")]
public class NodeAllocationsController : Controller
{
private readonly DatabaseRepository<Node> NodeRepository;
@@ -27,63 +26,58 @@ public class NodeAllocationsController : Controller
AllocationRepository = allocationRepository;
}
[HttpGet("{nodeId:int}/allocations")]
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<IPagedData<NodeAllocationDetailResponse>> Get(
public async Task<ActionResult<CountedData<NodeAllocationResponse>>> GetAsync(
[FromRoute] int nodeId,
[FromQuery] int page,
[FromQuery] [Range(1, 100)] int pageSize
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var count = await AllocationRepository.Get().CountAsync(x => x.Node.Id == nodeId);
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var totalCount = await AllocationRepository
.Get()
.CountAsync(x => x.Node.Id == nodeId);
var allocations = await AllocationRepository
.Get()
.Skip(page * pageSize)
.Take(pageSize)
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.Where(x => x.Node.Id == nodeId)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
var mappedAllocations = allocations.Select(x => new NodeAllocationDetailResponse()
return new CountedData<NodeAllocationResponse>()
{
Id = x.Id,
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray();
return new PagedData<NodeAllocationDetailResponse>()
{
Items = mappedAllocations,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
Items = allocations,
TotalCount = totalCount
};
}
[HttpGet("{nodeId:int}/allocations/{id:int}")]
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<NodeAllocationDetailResponse> GetSingle([FromRoute] int nodeId, [FromRoute] int id)
public async Task<ActionResult<NodeAllocationResponse>> GetSingleAsync([FromRoute] int nodeId, [FromRoute] int id)
{
var allocation = await AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.AsNoTracking()
.ProjectToAdminResponse()
.FirstOrDefaultAsync(x => x.Id == id);
if (allocation == null)
throw new HttpApiException("No allocation with that id found", 404);
return Problem("No allocation with that id found", statusCode: 400);
return new()
{
Id = allocation.Id,
IpAddress = allocation.IpAddress,
Port = allocation.Port
};
return allocation;
}
[HttpPost("{nodeId:int}/allocations")]
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
public async Task<NodeAllocationDetailResponse> Create(
public async Task<ActionResult<NodeAllocationResponse>> CreateAsync(
[FromRoute] int nodeId,
[FromBody] CreateNodeAllocationRequest request
)
@@ -93,28 +87,21 @@ public class NodeAllocationsController : Controller
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (node == null)
throw new HttpApiException("No node with that id found", 404);
return Problem("No node with that id found", statusCode: 404);
var allocation = new Allocation
{
IpAddress = request.IpAddress,
Port = request.Port,
Node = node
};
var allocation = AllocationMapper.ToAllocation(request);
var finalVariable = await AllocationRepository.Add(allocation);
var finalAllocation = await AllocationRepository.AddAsync(allocation);
return new()
{
Id = finalVariable.Id,
IpAddress = finalVariable.IpAddress,
Port = finalVariable.Port
};
return AllocationMapper.ToNodeAllocation(finalAllocation);
}
[HttpPatch("{nodeId:int}/allocations/{id:int}")]
public async Task<NodeAllocationDetailResponse> Update([FromRoute] int nodeId, [FromRoute] int id,
[FromBody] UpdateNodeAllocationRequest request)
[HttpPatch("{id:int}")]
public async Task<ActionResult<NodeAllocationResponse>> UpdateAsync(
[FromRoute] int nodeId,
[FromRoute] int id,
[FromBody] UpdateNodeAllocationRequest request
)
{
var allocation = await AllocationRepository
.Get()
@@ -122,23 +109,16 @@ public class NodeAllocationsController : Controller
.FirstOrDefaultAsync(x => x.Id == id);
if (allocation == null)
throw new HttpApiException("No allocation with that id found", 404);
return Problem("No allocation with that id found", statusCode: 404);
allocation.IpAddress = request.IpAddress;
allocation.Port = request.Port;
AllocationMapper.Merge(request, allocation);
await AllocationRepository.UpdateAsync(allocation);
await AllocationRepository.Update(allocation);
return new()
{
Id = allocation.Id,
IpAddress = allocation.IpAddress,
Port = allocation.Port
};
return AllocationMapper.ToNodeAllocation(allocation);
}
[HttpDelete("{nodeId:int}/allocations/{id:int}")]
public async Task Delete([FromRoute] int nodeId, [FromRoute] int id)
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteAsync([FromRoute] int nodeId, [FromRoute] int id)
{
var allocation = await AllocationRepository
.Get()
@@ -146,32 +126,44 @@ public class NodeAllocationsController : Controller
.FirstOrDefaultAsync(x => x.Id == id);
if (allocation == null)
throw new HttpApiException("No allocation with that id found", 404);
return Problem("No allocation with that id found", statusCode: 404);
await AllocationRepository.Remove(allocation);
await AllocationRepository.RemoveAsync(allocation);
return NoContent();
}
[HttpPost("{nodeId:int}/allocations/range")]
public async Task CreateRange([FromRoute] int nodeId, [FromBody] CreateNodeAllocationRangeRequest rangeRequest)
[HttpPost("range")]
public async Task<ActionResult> CreateRangeAsync(
[FromRoute] int nodeId,
[FromBody] CreateNodeAllocationRangeRequest request
)
{
if (request.Start > request.End)
return Problem("Invalid start and end specified", statusCode: 400);
if (request.End - request.Start == 0)
return Problem("Empty range specified", statusCode: 400);
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (node == null)
throw new HttpApiException("No node with that id found", 404);
return Problem("No node with that id found", statusCode: 404);
var existingAllocations = AllocationRepository
var existingAllocations = await AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.ToArray();
.Where(x => x.Port >= request.Start && x.Port <= request.End &&
x.IpAddress == request.IpAddress)
.AsNoTracking()
.ToArrayAsync();
var ports = new List<int>();
for (var i = rangeRequest.Start; i < rangeRequest.End; i++)
for (var i = request.Start; i < request.End; i++)
{
// Skip existing allocations
if (existingAllocations.Any(x => x.Port == i && x.IpAddress == rangeRequest.IpAddress))
if (existingAllocations.Any(x => x.Port == i))
continue;
ports.Add(i);
@@ -180,60 +172,66 @@ public class NodeAllocationsController : Controller
var allocations = ports
.Select(port => new Allocation()
{
IpAddress = rangeRequest.IpAddress,
IpAddress = request.IpAddress,
Port = port,
Node = node
})
.ToArray();
await AllocationRepository.RunTransaction(async set => { await set.AddRangeAsync(allocations); });
await AllocationRepository.RunTransactionAsync(async set => { await set.AddRangeAsync(allocations); });
return NoContent();
}
[HttpDelete("{nodeId:int}/allocations/all")]
public async Task DeleteAll([FromRoute] int nodeId)
[HttpDelete("all")]
public async Task<ActionResult> DeleteAllAsync([FromRoute] int nodeId)
{
var allocations = AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.ToArray();
await AllocationRepository.RunTransaction(set => { set.RemoveRange(allocations); });
await AllocationRepository.RunTransactionAsync(set => { set.RemoveRange(allocations); });
return NoContent();
}
[HttpGet("{nodeId:int}/allocations/free")]
[HttpGet("free")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<IPagedData<NodeAllocationDetailResponse>> GetFree([FromRoute] int nodeId, [FromQuery] int page,
[FromQuery][Range(1, 100)] int pageSize, [FromQuery] int serverId = -1)
public async Task<ActionResult<CountedData<NodeAllocationResponse>>> GetFreeAsync(
[FromRoute] int nodeId,
[FromQuery] int startIndex,
[FromQuery] int count,
[FromQuery] int serverId = -1
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var node = NodeRepository
.Get()
.FirstOrDefault(x => x.Id == nodeId);
if (node == null)
throw new HttpApiException("A node with this id could not be found", 404);
return Problem("A node with this id could not be found", statusCode: 404);
var freeAllocationsQuery = AllocationRepository
.Get()
.OrderBy(x => x.Id)
.Where(x => x.Node.Id == node.Id)
.Where(x => x.Server == null || x.Server.Id == serverId);
var count = await freeAllocationsQuery.CountAsync();
var allocations = await freeAllocationsQuery.ToArrayAsync();
var totalCount = await freeAllocationsQuery.CountAsync();
var mappedAllocations = allocations.Select(x => new NodeAllocationDetailResponse()
{
Id = x.Id,
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray();
var allocations = await freeAllocationsQuery
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new PagedData<NodeAllocationDetailResponse>()
return new CountedData<NodeAllocationResponse>()
{
Items = mappedAllocations,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
Items = allocations,
TotalCount = totalCount
};
}
}

View File

@@ -1,8 +1,8 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys;
@@ -22,11 +22,14 @@ public class NodeStatusController : Controller
NodeService = nodeService;
}
[HttpGet("{nodeId}/system/status")]
[HttpGet("{nodeId:int}/system/status")]
[Authorize(Policy = "permissions:admin.servers.nodes.status")]
public async Task<NodeSystemStatusResponse> GetStatus([FromRoute] int nodeId)
public async Task<ActionResult<NodeSystemStatusResponse>> GetStatusAsync([FromRoute] int nodeId)
{
var node = GetNode(nodeId);
var node = await GetNodeAsync(nodeId);
if (node.Value == null)
return node.Result ?? Problem("Unable to retrieve node");
NodeSystemStatusResponse response;
@@ -35,7 +38,7 @@ public class NodeStatusController : Controller
try
{
var statusResponse = await NodeService.GetSystemStatus(node);
var statusResponse = await NodeService.GetSystemStatusAsync(node.Value);
sw.Stop();
@@ -65,14 +68,14 @@ public class NodeStatusController : Controller
return response;
}
private Node GetNode(int nodeId)
private async Task<ActionResult<Node>> GetNodeAsync(int nodeId)
{
var result = NodeRepository
var result = await NodeRepository
.Get()
.FirstOrDefault(x => x.Id == nodeId);
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (result == null)
throw new HttpApiException("A node with this id could not be found", 404);
return Problem("A node with this id could not be found", statusCode: 404);
return result;
}

View File

@@ -1,13 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Common;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.Nodes;
using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations;
using MoonlightServers.Shared.Http.Responses.Admin.Nodes;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes;
@@ -16,57 +15,100 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes;
[Route("api/admin/servers/nodes")]
public class NodesController : Controller
{
private readonly CrudHelper<Node, NodeDetailResponse> CrudHelper;
private readonly DatabaseRepository<Node> NodeRepository;
public NodesController(
CrudHelper<Node, NodeDetailResponse> crudHelper,
DatabaseRepository<Node> nodeRepository
)
public NodesController(DatabaseRepository<Node> nodeRepository)
{
CrudHelper = crudHelper;
NodeRepository = nodeRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<IPagedData<NodeDetailResponse>> Get([FromQuery] int page, [FromQuery] int pageSize)
public async Task<ActionResult<CountedData<NodeResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
return await CrudHelper.Get(page, pageSize);
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var totalCount = await NodeRepository.Get().CountAsync();
var items = await NodeRepository
.Get()
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<NodeResponse>()
{
Items = items,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<NodeDetailResponse> GetSingle([FromRoute] int id)
public async Task<ActionResult<NodeResponse>> GetSingleAsync([FromRoute] int id)
{
return await CrudHelper.GetSingle(id);
var node = await NodeRepository
.Get()
.AsNoTracking()
.ProjectToAdminResponse()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
return node;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
public async Task<NodeDetailResponse> Create([FromBody] CreateNodeRequest request)
public async Task<ActionResult<NodeResponse>> CreateAsync([FromBody] CreateNodeRequest request)
{
var node = Mapper.Map<Node>(request);
var node = NodeMapper.ToNode(request);
node.TokenId = Formatter.GenerateString(6);
node.Token = Formatter.GenerateString(32);
var finalNode = await NodeRepository.Add(node);
var finalNode = await NodeRepository.AddAsync(node);
return CrudHelper.MapToResult(finalNode);
return NodeMapper.ToAdminNodeResponse(finalNode);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.update")]
public async Task<NodeDetailResponse> Update([FromRoute] int id, [FromBody] UpdateNodeRequest request)
public async Task<ActionResult<NodeResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateNodeRequest request)
{
return await CrudHelper.Update(id, request);
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
NodeMapper.Merge(request, node);
await NodeRepository.UpdateAsync(node);
return NodeMapper.ToAdminNodeResponse(node);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.delete")]
public async Task Delete([FromRoute] int id)
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
await CrudHelper.Delete(id);
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
await NodeRepository.RemoveAsync(node);
return Ok();
}
}

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
@@ -10,7 +10,7 @@ using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes;
[ApiController]
[Route("api/admin/servers/nodes")]
[Route("api/admin/servers/nodes/{nodeId:int}/statistics")]
[Authorize(Policy = "permissions:admin.servers.nodes.statistics")]
public class StatisticsController : Controller
{
@@ -23,13 +23,19 @@ public class StatisticsController : Controller
NodeRepository = nodeRepository;
}
[HttpGet("{nodeId:int}/statistics")]
public async Task<StatisticsResponse> Get([FromRoute] int nodeId)
[HttpGet]
[SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action", MessageId = "time: 1142ms",
Justification = "The daemon has an artificial delay of one second to calculate accurate cpu usage values")]
public async Task<ActionResult<StatisticsResponse>> GetAsync([FromRoute] int nodeId)
{
var node = await GetNode(nodeId);
var statistics = await NodeService.GetStatistics(node);
var node = await GetNodeAsync(nodeId);
return new()
if (node.Value == null)
return node.Result ?? Problem("Unable to retrieve node");
var statistics = await NodeService.GetStatisticsAsync(node.Value);
return new StatisticsResponse()
{
Cpu = new()
{
@@ -58,13 +64,17 @@ public class StatisticsController : Controller
};
}
[HttpGet("{nodeId:int}/statistics/docker")]
public async Task<DockerStatisticsResponse> GetDocker([FromRoute] int nodeId)
[HttpGet("docker")]
public async Task<ActionResult<DockerStatisticsResponse>> GetDockerAsync([FromRoute] int nodeId)
{
var node = await GetNode(nodeId);
var statistics = await NodeService.GetDockerStatistics(node);
var node = await GetNodeAsync(nodeId);
return new()
if (node.Value == null)
return node.Result ?? Problem("Unable to retrieve node");
var statistics = await NodeService.GetDockerStatisticsAsync(node.Value);
return new DockerStatisticsResponse()
{
BuildCacheReclaimable = statistics.BuildCacheReclaimable,
BuildCacheUsed = statistics.BuildCacheUsed,
@@ -76,14 +86,14 @@ public class StatisticsController : Controller
};
}
private async Task<Node> GetNode(int nodeId)
private async Task<ActionResult<Node>> GetNodeAsync(int nodeId)
{
var result = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (result == null)
throw new HttpApiException("A node with this id could not be found", 404);
return Problem("A node with this id could not be found", statusCode: 404);
return result;
}

View File

@@ -1,11 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Exceptions;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Responses.Admin.ServerVariables;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Servers;
@@ -17,32 +16,51 @@ public class ServerVariablesController : Controller
private readonly DatabaseRepository<ServerVariable> VariableRepository;
private readonly DatabaseRepository<Server> ServerRepository;
public ServerVariablesController(DatabaseRepository<ServerVariable> variableRepository, DatabaseRepository<Server> serverRepository)
public ServerVariablesController(
DatabaseRepository<ServerVariable> variableRepository,
DatabaseRepository<Server> serverRepository
)
{
VariableRepository = variableRepository;
ServerRepository = serverRepository;
}
[HttpGet("{serverId}/variables")]
[Authorize(Policy = "permissions:admin.servers.get")]
public async Task<PagedData<ServerVariableDetailResponse>> Get([FromRoute] int serverId, [FromQuery] int page, [FromQuery] int pageSize)
[HttpGet("{serverId:int}/variables")]
[Authorize(Policy = "permissions:admin.servers.read")]
public async Task<ActionResult<CountedData<ServerVariableResponse>>> GetAsync(
[FromRoute] int serverId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var server = await ServerRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == serverId);
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var variables = await VariableRepository
var serverExists = await ServerRepository
.Get()
.Where(x => x.Server.Id == server.Id)
.AnyAsync(x => x.Id == serverId);
if (!serverExists)
return Problem("No server with this id found", statusCode: 404);
var query = VariableRepository
.Get()
.Where(x => x.Server.Id == serverId);
var totalCount = await query.CountAsync();
var variables = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
var castedVariables = variables
.Select(x => Mapper.Map<ServerVariableDetailResponse>(x))
.ToArray();
return PagedData<ServerVariableDetailResponse>.Create(castedVariables, page, pageSize);
return new CountedData<ServerVariableResponse>()
{
Items = variables,
TotalCount = totalCount
};
}
}

View File

@@ -1,13 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Exceptions;
using Microsoft.Extensions.Logging;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Helpers;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Requests.Admin.Servers;
using MoonlightServers.Shared.Http.Responses.Admin.Servers;
@@ -18,7 +17,6 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Servers;
[Route("api/admin/servers")]
public class ServersController : Controller
{
private readonly CrudHelper<Server, ServerDetailResponse> CrudHelper;
private readonly DatabaseRepository<Star> StarRepository;
private readonly DatabaseRepository<Node> NodeRepository;
private readonly DatabaseRepository<Allocation> AllocationRepository;
@@ -29,7 +27,6 @@ public class ServersController : Controller
private readonly ServerService ServerService;
public ServersController(
CrudHelper<Server, ServerDetailResponse> crudHelper,
DatabaseRepository<Star> starRepository,
DatabaseRepository<Node> nodeRepository,
DatabaseRepository<Allocation> allocationRepository,
@@ -40,7 +37,6 @@ public class ServersController : Controller
ServerService serverService
)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
NodeRepository = nodeRepository;
AllocationRepository = allocationRepository;
@@ -49,48 +45,70 @@ public class ServersController : Controller
UserRepository = userRepository;
ServerService = serverService;
Logger = logger;
CrudHelper.QueryModifier = servers => servers
.Include(x => x.Node)
.Include(x => x.Allocations)
.Include(x => x.Variables)
.Include(x => x.Star);
CrudHelper.LateMapper = (server, response) =>
{
response.NodeId = server.Node.Id;
response.StarId = server.Star.Id;
response.AllocationIds = server.Allocations.Select(x => x.Id).ToArray();
return response;
};
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.get")]
public async Task<IPagedData<ServerDetailResponse>> Get([FromQuery] int page, [FromQuery] int pageSize)
[Authorize(Policy = "permissions:admin.servers.read")]
public async Task<ActionResult<CountedData<ServerResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
return await CrudHelper.Get(page, pageSize);
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var totalCount = await ServerRepository.Get().CountAsync();
var servers = await ServerRepository
.Get()
.Include(x => x.Node)
.Include(x => x.Allocations)
.Include(x => x.Variables)
.Include(x => x.Star)
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<ServerResponse>()
{
Items = servers,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.get")]
public async Task<ServerDetailResponse> GetSingle([FromRoute] int id)
[Authorize(Policy = "permissions:admin.servers.read")]
public async Task<ActionResult<ServerResponse>> GetSingleAsync([FromRoute] int id)
{
return await CrudHelper.GetSingle(id);
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.Include(x => x.Allocations)
.Include(x => x.Variables)
.Include(x => x.Star)
.AsNoTracking()
.Where(x => x.Id == id)
.ProjectToAdminResponse()
.FirstOrDefaultAsync();
if (server == null)
return Problem("No server with that id found", statusCode: 404);
return server;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.create")]
public async Task<ServerDetailResponse> Create([FromBody] CreateServerRequest request)
[Authorize(Policy = "permissions:admin.servers.write")]
public async Task<ActionResult<ServerResponse>> CreateAsync([FromBody] CreateServerRequest request)
{
// Construct model
var server = Mapper.Map<Server>(request);
// Check if owner user exist
if (UserRepository.Get().All(x => x.Id != request.OwnerId))
throw new HttpApiException("No user with this id found", 400);
return Problem("No user with this id found", statusCode: 400);
// Check if the star exists
var star = await StarRepository
.Get()
.Include(x => x.Variables)
@@ -98,14 +116,14 @@ public class ServersController : Controller
.FirstOrDefaultAsync(x => x.Id == request.StarId);
if (star == null)
throw new HttpApiException("No star with this id found", 400);
return Problem("No star with this id found", statusCode: 400);
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == request.NodeId);
if (node == null)
throw new HttpApiException("No node with this id found", 400);
return Problem("No node with this id found", statusCode: 400);
var allocations = new List<Allocation>();
@@ -140,13 +158,15 @@ public class ServersController : Controller
if (allocations.Count < star.RequiredAllocations)
{
throw new HttpApiException(
return Problem(
$"Unable to find enough free allocations. Found: {allocations.Count}, Required: {star.RequiredAllocations}",
400
statusCode: 400
);
}
}
var server = ServerMapper.ToServer(request);
// Set allocations
server.Allocations = allocations;
@@ -170,34 +190,47 @@ public class ServersController : Controller
server.Node = node;
server.Star = star;
var finalServer = await ServerRepository.Add(server);
var finalServer = await ServerRepository.AddAsync(server);
try
{
await ServerService.Sync(finalServer);
await ServerService.SyncAsync(finalServer);
}
catch (Exception e)
{
Logger.LogError("Unable to sync server to node the server is assigned to: {e}", e);
// We are deleting the server from the database after the creation has failed
// to ensure we wont have a bugged server in the database which doesnt exist on the node
await ServerRepository.Remove(finalServer);
// to ensure we won't have a bugged server in the database which doesn't exist on the node
await ServerRepository.RemoveAsync(finalServer);
throw;
}
return CrudHelper.MapToResult(finalServer);
return ServerMapper.ToAdminServerResponse(finalServer);
}
[HttpPatch("{id:int}")]
public async Task<ServerDetailResponse> Update([FromRoute] int id, [FromBody] UpdateServerRequest request)
[Authorize(Policy = "permissions:admin.servers.write")]
public async Task<ActionResult<ServerResponse>> UpdateAsync(
[FromRoute] int id,
[FromBody] UpdateServerRequest request
)
{
//TODO: Handle shrinking virtual disk
var server = await CrudHelper.GetSingleModel(id);
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.Include(x => x.Allocations)
.Include(x => x.Variables)
.Include(x => x.Star)
.FirstOrDefaultAsync(x => x.Id == id);
server = Mapper.Map(server, request);
if (server == null)
return Problem("No server with that id found", statusCode: 404);
ServerMapper.Merge(request, server);
var allocations = new List<Allocation>();
@@ -222,9 +255,9 @@ public class ServersController : Controller
// Check if the specified allocations are enough for the star
if (allocations.Count < server.Star.RequiredAllocations)
{
throw new HttpApiException(
return Problem(
$"You need to specify at least {server.Star.RequiredAllocations} allocation(s)",
400
statusCode: 400
);
}
@@ -245,24 +278,38 @@ public class ServersController : Controller
serverVar.Value = variable.Value;
}
await ServerRepository.Update(server);
await ServerRepository.UpdateAsync(server);
// Notify the node about the changes
await ServerService.Sync(server);
await ServerService.SyncAsync(server);
return CrudHelper.MapToResult(server);
return ServerMapper.ToAdminServerResponse(server);
}
[HttpDelete("{id:int}")]
public async Task Delete([FromRoute] int id, [FromQuery] bool force = false)
public async Task<ActionResult> DeleteAsync([FromRoute] int id, [FromQuery] bool force = false)
{
var server = await CrudHelper.GetSingleModel(id);
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.Include(x => x.Star)
.Include(x => x.Variables)
.Include(x => x.Backups)
.Include(x => x.Allocations)
.FirstOrDefaultAsync(x => x.Id == id);
if (server == null)
return Problem("No server with that id found", statusCode: 404);
server.Variables.Clear();
server.Backups.Clear();
server.Allocations.Clear();
try
{
// If the sync fails on the node and we aren't forcing the deletion,
// we don't want to delete it from the database yet
await ServerService.SyncDelete(server);
await ServerService.SyncDeleteAsync(server);
}
catch (Exception e)
{
@@ -277,6 +324,7 @@ public class ServersController : Controller
throw;
}
await CrudHelper.Delete(id);
await ServerRepository.RemoveAsync(server);
return NoContent();
}
}

View File

@@ -1,101 +1,159 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonCore.Common;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.StarDockerImages;
using MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
[ApiController]
[Route("api/admin/servers/stars")]
[Route("api/admin/servers/stars/{starId:int}/dockerImages")]
public class StarDockerImagesController : Controller
{
private readonly CrudHelper<StarDockerImage, StarDockerImageDetailResponse> CrudHelper;
private readonly DatabaseRepository<Star> StarRepository;
private readonly DatabaseRepository<StarDockerImage> StarDockerImageRepository;
private Star Star;
private readonly DatabaseRepository<StarDockerImage> DockerImageRepository;
public StarDockerImagesController(
CrudHelper<StarDockerImage, StarDockerImageDetailResponse> crudHelper,
DatabaseRepository<Star> starRepository,
DatabaseRepository<StarDockerImage> starDockerImageRepository
DatabaseRepository<StarDockerImage> dockerImageRepository
)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
StarDockerImageRepository = starDockerImageRepository;
DockerImageRepository = dockerImageRepository;
}
private async Task ApplyStar(int id)
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<ActionResult<CountedData<StarDockerImageResponse>>> GetAsync(
[FromRoute] int starId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var query = DockerImageRepository
.Get()
.Where(x => x.Star.Id == starId);
var totalCount = await query.CountAsync();
var dockerImages = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<StarDockerImageResponse>()
{
Items = dockerImages,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.read")]
public async Task<ActionResult<StarDockerImageResponse>> GetSingleAsync([FromRoute] int starId, [FromRoute] int id)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var dockerImage = await DockerImageRepository
.Get()
.Where(x => x.Id == id && x.Star.Id == starId)
.ProjectToAdminResponse()
.FirstOrDefaultAsync();
if (dockerImage == null)
return Problem("No star docker image with this id found", statusCode: 404);
return dockerImage;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.stars.write")]
public async Task<ActionResult<StarDockerImageResponse>> CreateAsync(
[FromRoute] int starId,
[FromBody] CreateStarDockerImageRequest request
)
{
var star = await StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
.FirstOrDefaultAsync(x => x.Id == starId);
if (star == null)
throw new HttpApiException("A star with this id could not be found", 404);
return Problem("No star with this id found", statusCode: 404);
Star = star;
var dockerImage = DockerImageMapper.ToDockerImage(request);
dockerImage.Star = star;
CrudHelper.QueryModifier = dockerImages =>
dockerImages.Where(x => x.Star.Id == star.Id);
var finalDockerImage = await DockerImageRepository.AddAsync(dockerImage);
return DockerImageMapper.ToAdminResponse(finalDockerImage);
}
[HttpGet("{starId:int}/dockerImages")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<IPagedData<StarDockerImageDetailResponse>> Get([FromRoute] int starId, [FromQuery] int page, [FromQuery] int pageSize)
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.write")]
public async Task<ActionResult<StarDockerImageResponse>> UpdateAsync(
[FromRoute] int starId,
[FromRoute] int id,
[FromBody] UpdateStarDockerImageRequest request
)
{
await ApplyStar(starId);
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
return await CrudHelper.Get(page, pageSize);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var dockerImage = await DockerImageRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
if (dockerImage == null)
return Problem("No star docker image with this id found", statusCode: 404);
DockerImageMapper.Merge(request, dockerImage);
await DockerImageRepository.UpdateAsync(dockerImage);
return DockerImageMapper.ToAdminResponse(dockerImage);
}
[HttpGet("{starId:int}/dockerImages/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<StarDockerImageDetailResponse> GetSingle([FromRoute] int starId, [FromRoute] int id)
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.write")]
public async Task<ActionResult> DeleteAsync([FromRoute] int starId, [FromRoute] int id)
{
await ApplyStar(starId);
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
return await CrudHelper.GetSingle(id);
}
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
[HttpPost("{starId:int}/dockerImages")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarDockerImageDetailResponse> Create([FromRoute] int starId, [FromBody] CreateStarDockerImageRequest request)
{
await ApplyStar(starId);
var dockerImage = await DockerImageRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
var starDockerImage = Mapper.Map<StarDockerImage>(request);
starDockerImage.Star = Star;
if (dockerImage == null)
return Problem("No star docker image with this id found", statusCode: 404);
var finalVariable = await StarDockerImageRepository.Add(starDockerImage);
return CrudHelper.MapToResult(finalVariable);
}
[HttpPatch("{starId:int}/dockerImages/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.update")]
public async Task<StarDockerImageDetailResponse> Update([FromRoute] int starId, [FromRoute] int id,
[FromBody] UpdateStarDockerImageRequest request)
{
await ApplyStar(starId);
return await CrudHelper.Update(id, request);
}
[HttpDelete("{starId:int}/dockerImages/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
public async Task Delete([FromRoute] int starId, [FromRoute] int id)
{
await ApplyStar(starId);
await CrudHelper.Delete(id);
await DockerImageRepository.RemoveAsync(dockerImage);
return NoContent();
}
}

View File

@@ -2,7 +2,7 @@ using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Responses.Admin.Stars;
@@ -21,18 +21,15 @@ public class StarImportExportController : Controller
[HttpGet("{starId:int}/export")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task Export([FromRoute] int starId)
public async Task<ActionResult> ExportAsync([FromRoute] int starId)
{
var exportedStar = await ImportExportService.Export(starId);
Response.StatusCode = 200;
Response.ContentType = "application/json";
await Response.WriteAsync(exportedStar);
var exportedStar = await ImportExportService.ExportAsync(starId);
return Content(exportedStar, "application/json");
}
[HttpPost("import")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarDetailResponse> Import()
public async Task<StarResponse> ImportAsync()
{
if (Request.Form.Files.Count == 0)
throw new HttpApiException("No file to import provided", 400);
@@ -45,8 +42,8 @@ public class StarImportExportController : Controller
using var sr = new StreamReader(stream, Encoding.UTF8);
var content = await sr.ReadToEndAsync();
var star = await ImportExportService.Import(content);
var star = await ImportExportService.ImportAsync(content);
return Mapper.Map<StarDetailResponse>(star);
return StarMapper.ToAdminResponse(star);
}
}

View File

@@ -1,106 +1,160 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Common;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.StarVariables;
using MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
[ApiController]
[Route("api/admin/servers/stars")]
[Route("api/admin/servers/stars/{starId:int}/variables")]
public class StarVariablesController : Controller
{
private readonly CrudHelper<StarVariable, StarVariableDetailResponse> CrudHelper;
private readonly DatabaseRepository<Star> StarRepository;
private readonly DatabaseRepository<StarVariable> StarVariableRepository;
private Star Star;
private readonly DatabaseRepository<StarVariable> VariableRepository;
public StarVariablesController(
CrudHelper<StarVariable, StarVariableDetailResponse> crudHelper,
DatabaseRepository<Star> starRepository,
DatabaseRepository<StarVariable> starVariableRepository)
DatabaseRepository<StarVariable> variableRepository)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
StarVariableRepository = starVariableRepository;
VariableRepository = variableRepository;
}
private async Task ApplyStar(int id)
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<ActionResult<CountedData<StarVariableResponse>>> GetAsync(
[FromRoute] int starId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var star = await StarRepository
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var starExists = StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
.Any(x => x.Id == starId);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var query = VariableRepository
.Get()
.Where(x => x.Star.Id == starId);
var totalCount = await query.CountAsync();
var variables = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<StarVariableResponse>()
{
Items = variables,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<StarVariableResponse> GetSingleAsync(
[FromRoute] int starId,
[FromRoute] int id
)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
throw new HttpApiException("No star with this id found", 404);
var starVariable = await VariableRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
if (starVariable == null)
throw new HttpApiException("No variable with this id found", 404);
return StarVariableMapper.ToAdminResponse(starVariable);
}
[HttpPost("")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarVariableResponse> CreateAsync([FromRoute] int starId,
[FromBody] CreateStarVariableRequest request)
{
var star = StarRepository
.Get()
.FirstOrDefault(x => x.Id == starId);
if (star == null)
throw new HttpApiException("A star with this id could not be found", 404);
throw new HttpApiException("No star with this id found", 404);
Star = star;
var starVariable = StarVariableMapper.ToStarVariable(request);
starVariable.Star = star;
CrudHelper.QueryModifier = variables =>
variables.Where(x => x.Star.Id == star.Id);
await VariableRepository.AddAsync(starVariable);
return StarVariableMapper.ToAdminResponse(starVariable);
}
[HttpGet("{starId:int}/variables")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<IPagedData<StarVariableDetailResponse>> Get([FromRoute] int starId, [FromQuery] int page, [FromQuery] int pageSize)
{
await ApplyStar(starId);
return await CrudHelper.Get(page, pageSize);
}
[HttpGet("{starId:int}/variables/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<StarVariableDetailResponse> GetSingle([FromRoute] int starId, [FromRoute] int id)
{
await ApplyStar(starId);
return await CrudHelper.GetSingle(id);
}
[HttpPost("{starId:int}/variables")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarVariableDetailResponse> Create([FromRoute] int starId, [FromBody] CreateStarVariableRequest request)
{
await ApplyStar(starId);
var starVariable = Mapper.Map<StarVariable>(request);
starVariable.Star = Star;
var finalVariable = await StarVariableRepository.Add(starVariable);
return CrudHelper.MapToResult(finalVariable);
}
[HttpPatch("{starId:int}/variables/{id:int}")]
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.update")]
public async Task<StarVariableDetailResponse> Update([FromRoute] int starId, [FromRoute] int id,
[FromBody] UpdateStarVariableRequest request)
public async Task<StarVariableResponse> UpdateAsync(
[FromRoute] int starId,
[FromRoute] int id,
[FromBody] UpdateStarVariableRequest request
)
{
await ApplyStar(starId);
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
var variable = await CrudHelper.GetSingleModel(id);
if (!starExists)
throw new HttpApiException("No star with this id found", 404);
variable = Mapper.Map(variable, request);
var starVariable = await VariableRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
await StarVariableRepository.Update(variable);
if (starVariable == null)
throw new HttpApiException("No variable with this id found", 404);
return CrudHelper.MapToResult(variable);
StarVariableMapper.Merge(request, starVariable);
await VariableRepository.UpdateAsync(starVariable);
return StarVariableMapper.ToAdminResponse(starVariable);
}
[HttpDelete("{starId:int}/variables/{id:int}")]
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
public async Task Delete([FromRoute] int starId, [FromRoute] int id)
public async Task DeleteAsync([FromRoute] int starId, [FromRoute] int id)
{
await ApplyStar(starId);
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
await CrudHelper.Delete(id);
if (!starExists)
throw new HttpApiException("No star with this id found", 404);
var starVariable = await VariableRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
if (starVariable == null)
throw new HttpApiException("No variable with this id found", 404);
await VariableRepository.RemoveAsync(starVariable);
}
}

View File

@@ -1,15 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.Stars;
using MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages;
using MoonlightServers.Shared.Http.Responses.Admin.Stars;
using MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
@@ -17,34 +14,62 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
[Route("api/admin/servers/stars")]
public class StarsController : Controller
{
private readonly CrudHelper<Star, StarDetailResponse> CrudHelper;
private readonly DatabaseRepository<Star> StarRepository;
public StarsController(CrudHelper<Star, StarDetailResponse> crudHelper, DatabaseRepository<Star> starRepository)
public StarsController(DatabaseRepository<Star> starRepository)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<IPagedData<StarDetailResponse>> Get([FromQuery] int page, [FromQuery] int pageSize)
[Authorize(Policy = "permissions:admin.servers.stars.read")]
public async Task<ActionResult<CountedData<StarResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
return await CrudHelper.Get(page, pageSize);
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var totalCount = await StarRepository.Get().CountAsync();
var stars = await StarRepository
.Get()
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<StarResponse>()
{
Items = stars,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<StarDetailResponse> GetSingle([FromRoute] int id)
[Authorize(Policy = "permissions:admin.servers.stars.read")]
public async Task<ActionResult<StarResponse>> GetSingleAsync([FromRoute] int id)
{
return await CrudHelper.GetSingle(id);
var star = await StarRepository
.Get()
.AsNoTracking()
.ProjectToAdminResponse()
.FirstOrDefaultAsync(x => x.Id == id);
if (star == null)
return Problem("No star with that id found", statusCode: 404);
return star;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarDetailResponse> Create([FromBody] CreateStarRequest request)
public async Task<ActionResult<StarResponse>> CreateAsync([FromBody] CreateStarRequest request)
{
var star = Mapper.Map<Star>(request);
var star = StarMapper.ToStar(request);
// Default values
star.DonateUrl = null;
@@ -61,22 +86,43 @@ public class StarsController : Controller
star.DefaultDockerImage = -1;
star.ParseConfiguration = "[]";
var finalStar = await StarRepository.Add(star);
var finalStar = await StarRepository.AddAsync(star);
return CrudHelper.MapToResult(finalStar);
return StarMapper.ToAdminResponse(finalStar);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.update")]
public async Task<StarDetailResponse> Update([FromRoute] int id, [FromBody] UpdateStarRequest request)
public async Task<ActionResult<StarResponse>> UpdateAsync(
[FromRoute] int id,
[FromBody] UpdateStarRequest request
)
{
return await CrudHelper.Update(id, request);
var star = await StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (star == null)
return Problem("No star with that id found", statusCode: 404);
StarMapper.Merge(request, star);
await StarRepository.UpdateAsync(star);
return StarMapper.ToAdminResponse(star);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
public async Task Delete([FromRoute] int id)
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
await CrudHelper.Delete(id);
var star = await StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (star == null)
return Problem("No star with that id found", statusCode: 404);
await StarRepository.RemoveAsync(star);
return NoContent();
}
}

View File

@@ -1,12 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.DaemonShared.Enums;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Files;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
@@ -15,7 +14,7 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers")]
[Route("api/client/servers/{serverId:int}/files")]
public class FilesController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
@@ -36,51 +35,83 @@ public class FilesController : Controller
AuthorizeService = authorizeService;
}
[HttpGet("{serverId:int}/files/list")]
public async Task<ServerFilesEntryResponse[]> List([FromRoute] int serverId, [FromQuery] string path)
[HttpGet("list")]
public async Task<ActionResult<ServerFilesEntryResponse[]>> ListAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.Read);
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.Read);
var entries = await ServerFileSystemService.List(server, path);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var entries = await ServerFileSystemService.ListAsync(server.Value, path);
return entries.Select(x => new ServerFilesEntryResponse()
{
Name = x.Name,
Size = x.Size,
IsFile = x.IsFile,
IsFolder = x.IsFolder,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
}).ToArray();
}
[HttpPost("{serverId:int}/files/move")]
public async Task Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath)
[HttpPost("move")]
public async Task<ActionResult> MoveAsync([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
await ServerFileSystemService.Move(server, oldPath, newPath);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerFileSystemService.MoveAsync(server.Value, oldPath, newPath);
return NoContent();
}
[HttpDelete("{serverId:int}/files/delete")]
public async Task Delete([FromRoute] int serverId, [FromQuery] string path)
[HttpDelete("delete")]
public async Task<ActionResult> DeleteAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
await ServerFileSystemService.Delete(server, path);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerFileSystemService.DeleteAsync(server.Value, path);
return NoContent();
}
[HttpPost("{serverId:int}/files/mkdir")]
public async Task Mkdir([FromRoute] int serverId, [FromQuery] string path)
[HttpPost("mkdir")]
public async Task<ActionResult> MkdirAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
await ServerFileSystemService.Mkdir(server, path);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerFileSystemService.MkdirAsync(server.Value, path);
return NoContent();
}
[HttpGet("{serverId:int}/files/upload")]
public async Task<ServerFilesUploadResponse> Upload([FromRoute] int serverId)
[HttpPost("touch")]
public async Task<ActionResult> TouchAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerFileSystemService.MkdirAsync(server.Value, path);
return NoContent();
}
[HttpGet("upload")]
public async Task<ActionResult<ServerFilesUploadResponse>> UploadAsync([FromRoute] int serverId)
{
var serverResult = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
var accessToken = NodeService.CreateAccessToken(
server.Node,
@@ -104,10 +135,15 @@ public class FilesController : Controller
};
}
[HttpGet("{serverId:int}/files/download")]
public async Task<ServerFilesDownloadResponse> Download([FromRoute] int serverId, [FromQuery] string path)
[HttpGet("download")]
public async Task<ActionResult<ServerFilesDownloadResponse>> DownloadAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.Read);
var serverResult = await GetServerByIdAsync(serverId, ServerPermissionLevel.Read);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
var accessToken = NodeService.CreateAccessToken(
server.Node,
@@ -132,29 +168,37 @@ public class FilesController : Controller
};
}
[HttpPost("{serverId:int}/files/compress")]
public async Task Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request)
[HttpPost("compress")]
public async Task<ActionResult> CompressAsync([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
if (!Enum.TryParse(request.Type, true, out CompressType type))
throw new HttpApiException("Invalid compress type provided", 400);
return Problem("Invalid compress type provided", statusCode: 400);
await ServerFileSystemService.Compress(server, type, request.Items, request.Destination);
await ServerFileSystemService.CompressAsync(server.Value, type, request.Items, request.Destination);
return Ok();
}
[HttpPost("{serverId:int}/files/decompress")]
public async Task Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request)
[HttpPost("decompress")]
public async Task<ActionResult> DecompressAsync([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
if (!Enum.TryParse(request.Type, true, out CompressType type))
throw new HttpApiException("Invalid compress type provided", 400);
return Problem("Invalid decompress type provided", statusCode: 400);
await ServerFileSystemService.Decompress(server, type, request.Path, request.Destination);
await ServerFileSystemService.DecompressAsync(server.Value, type, request.Path, request.Destination);
return NoContent();
}
private async Task<Server> GetServerById(int serverId, ServerPermissionType type)
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId, ServerPermissionLevel level)
{
var server = await ServerRepository
.Get()
@@ -162,18 +206,19 @@ public class FilesController : Controller
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.Authorize(
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
permission => permission.Name == "files" && permission.Type >= type
ServerPermissionConstants.Files,
level
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
403
statusCode: 403
);
}

View File

@@ -1,64 +1,74 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[ApiController]
[Authorize]
[Route("api/client/servers")]
[Route("api/client/servers/{serverId:int}")]
public class PowerController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly ServerService ServerService;
private readonly ServerAuthorizeService AuthorizeService;
public PowerController(
DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository,
ServerService serverService,
ServerAuthorizeService authorizeService
)
{
ServerRepository = serverRepository;
UserRepository = userRepository;
ServerService = serverService;
AuthorizeService = authorizeService;
}
[HttpPost("{serverId:int}/start")]
[HttpPost("start")]
[Authorize]
public async Task Start([FromRoute] int serverId)
public async Task<ActionResult> StartAsync([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Start(server);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.StartAsync(server.Value);
return NoContent();
}
[HttpPost("{serverId:int}/stop")]
[HttpPost("stop")]
[Authorize]
public async Task Stop([FromRoute] int serverId)
public async Task<ActionResult> StopAsync([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Stop(server);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.StopAsync(server.Value);
return NoContent();
}
[HttpPost("{serverId:int}/kill")]
[HttpPost("kill")]
[Authorize]
public async Task Kill([FromRoute] int serverId)
public async Task<ActionResult> KillAsync([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Kill(server);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.KillAsync(server.Value);
return NoContent();
}
private async Task<Server> GetServerById(int serverId)
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId)
{
var server = await ServerRepository
.Get()
@@ -66,18 +76,19 @@ public class PowerController : Controller
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.Authorize(
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
permission => permission.Name == "power" && permission.Type >= ServerPermissionType.ReadWrite
ServerPermissionConstants.Power,
ServerPermissionLevel.ReadWrite
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
403
statusCode: 403
);
}

View File

@@ -2,18 +2,18 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Extensions;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Allocations;
using MoonlightServers.Shared.Models;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
@@ -47,12 +47,18 @@ public class ServersController : Controller
}
[HttpGet]
public async Task<PagedData<ServerDetailResponse>> GetAll([FromQuery] int page, [FromQuery] int pageSize)
public async Task<ActionResult<CountedData<ServerDetailResponse>>> GetAllAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var userIdClaim = User.FindFirstValue("userId");
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var userIdClaim = User.FindFirstValue("UserId");
if (string.IsNullOrEmpty(userIdClaim))
throw new HttpApiException("Only users are able to use this endpoint", 400);
return Problem("Only users are able to use this endpoint", statusCode: 400);
var userId = int.Parse(userIdClaim);
@@ -63,8 +69,14 @@ public class ServersController : Controller
.Include(x => x.Node)
.Where(x => x.OwnerId == userId);
var count = await query.CountAsync();
var items = await query.Skip(page * pageSize).Take(pageSize).ToArrayAsync();
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ToArrayAsync();
var mappedItems = items.Select(x => new ServerDetailResponse()
{
@@ -83,23 +95,26 @@ public class ServersController : Controller
}).ToArray()
}).ToArray();
return new PagedData<ServerDetailResponse>()
return new CountedData<ServerDetailResponse>()
{
Items = mappedItems,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : count / pageSize
TotalCount = totalCount
};
}
[HttpGet("shared")]
public async Task<PagedData<ServerDetailResponse>> GetAllShared([FromQuery] int page, [FromQuery] int pageSize)
public async Task<ActionResult<CountedData<ServerDetailResponse>>> GetAllSharedAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var userIdClaim = User.FindFirstValue("userId");
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var userIdClaim = User.FindFirstValue("UserId");
if (string.IsNullOrEmpty(userIdClaim))
throw new HttpApiException("Only users are able to use this endpoint", 400);
return Problem("Only users are able to use this endpoint", statusCode: 400);
var userId = int.Parse(userIdClaim);
@@ -113,8 +128,13 @@ public class ServersController : Controller
.ThenInclude(x => x.Allocations)
.Where(x => x.UserId == userId);
var count = await query.CountAsync();
var items = await query.Skip(page * pageSize).Take(pageSize).ToArrayAsync();
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.ToArrayAsync();
var ownerIds = items
.Select(x => x.Server.OwnerId)
@@ -144,22 +164,19 @@ public class ServersController : Controller
Share = new()
{
SharedBy = owners.First(y => y.Id == x.Server.OwnerId).Username,
Permissions = x.Content.Permissions.ToArray()
Permissions = ShareMapper.MapToPermissionLevels(x.Content)
}
}).ToArray();
return new PagedData<ServerDetailResponse>()
return new CountedData<ServerDetailResponse>()
{
Items = mappedItems,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : count / pageSize
TotalCount = totalCount
};
}
[HttpGet("{serverId:int}")]
public async Task<ServerDetailResponse> Get([FromRoute] int serverId)
public async Task<ActionResult<ServerDetailResponse>> GetAsync([FromRoute] int serverId)
{
var server = await ServerRepository
.Get()
@@ -169,15 +186,20 @@ public class ServersController : Controller
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var authorizationResult = await AuthorizeService.Authorize(User, server);
var authorizationResult = await AuthorizeService.AuthorizeAsync(
User,
server,
String.Empty,
ServerPermissionLevel.None
);
if (!authorizationResult.Succeeded)
{
throw new HttpApiException(
return Problem(
authorizationResult.Message ?? "No server with this id found",
404
statusCode: 404
);
}
@@ -209,7 +231,7 @@ public class ServersController : Controller
response.Share = new()
{
SharedBy = owner.Username,
Permissions = authorizationResult.Share.Content.Permissions.ToArray()
Permissions = ShareMapper.MapToPermissionLevels(authorizationResult.Share.Content)
};
}
@@ -217,11 +239,18 @@ public class ServersController : Controller
}
[HttpGet("{serverId:int}/status")]
public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
public async Task<ActionResult<ServerStatusResponse>> GetStatusAsync([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
var server = await GetServerByIdAsync(
serverId,
ServerPermissionConstants.Console,
ServerPermissionLevel.None
);
var status = await ServerService.GetStatus(server);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var status = await ServerService.GetStatusAsync(server.Value);
return new ServerStatusResponse()
{
@@ -230,13 +259,19 @@ public class ServersController : Controller
}
[HttpGet("{serverId:int}/ws")]
public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId)
public async Task<ActionResult<ServerWebSocketResponse>> GetWebSocketAsync([FromRoute] int serverId)
{
var server = await GetServerById(
var serverResult = await GetServerByIdAsync(
serverId,
permission => permission is { Name: "console", Type: >= ServerPermissionType.Read }
ServerPermissionConstants.Console,
ServerPermissionLevel.Read
);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
// TODO: Handle transparent node proxy
var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
@@ -258,14 +293,18 @@ public class ServersController : Controller
}
[HttpGet("{serverId:int}/logs")]
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
public async Task<ActionResult<ServerLogsResponse>> GetLogsAsync([FromRoute] int serverId)
{
var server = await GetServerById(
var server = await GetServerByIdAsync(
serverId,
permission => permission is { Name: "console", Type: >= ServerPermissionType.Read }
ServerPermissionConstants.Console,
ServerPermissionLevel.Read
);
var logs = await ServerService.GetLogs(server);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var logs = await ServerService.GetLogsAsync(server.Value);
return new ServerLogsResponse()
{
@@ -274,13 +313,18 @@ public class ServersController : Controller
}
[HttpGet("{serverId:int}/stats")]
public async Task<ServerStatsResponse> GetStats([FromRoute] int serverId)
public async Task<ActionResult<ServerStatsResponse>> GetStatsAsync([FromRoute] int serverId)
{
var server = await GetServerById(
serverId
var server = await GetServerByIdAsync(
serverId,
ServerPermissionConstants.Console,
ServerPermissionLevel.Read
);
var stats = await ServerService.GetStats(server);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var stats = await ServerService.GetStatsAsync(server.Value);
return new ServerStatsResponse()
{
@@ -293,7 +337,25 @@ public class ServersController : Controller
};
}
private async Task<Server> GetServerById(int serverId, Func<ServerSharePermission, bool>? filter = null)
[HttpPost("{serverId:int}/command")]
public async Task<ActionResult> CommandAsync([FromRoute] int serverId, [FromBody] ServerCommandRequest request)
{
var server = await GetServerByIdAsync(
serverId,
ServerPermissionConstants.Console,
ServerPermissionLevel.ReadWrite
);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.RunCommandAsync(server.Value, request.Command);
return NoContent();
}
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId, string permissionId,
ServerPermissionLevel level)
{
var server = await ServerRepository
.Get()
@@ -301,15 +363,15 @@ public class ServersController : Controller
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.Authorize(User, server, filter);
var authorizeResult = await AuthorizeService.AuthorizeAsync(User, server, permissionId, level);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
403
statusCode: 403
);
}

View File

@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
@@ -31,13 +31,18 @@ public class SettingsController : Controller
[HttpPost("{serverId:int}/install")]
[Authorize]
public async Task Install([FromRoute] int serverId)
public async Task<ActionResult> InstallAsync([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Install(server);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.InstallAsync(server.Value);
return NoContent();
}
private async Task<Server> GetServerById(int serverId)
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId)
{
var server = await ServerRepository
.Get()
@@ -45,18 +50,19 @@ public class SettingsController : Controller
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.Authorize(
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
permission => permission is { Name: "settings", Type: >= ServerPermissionType.ReadWrite }
ServerPermissionConstants.Settings,
ServerPermissionLevel.ReadWrite
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
403
statusCode: 403
);
}

View File

@@ -1,13 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Shares;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Shares;
@@ -16,7 +16,7 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers")]
[Route("api/client/servers/{serverId:int}/shares")]
public class SharesController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
@@ -37,21 +37,32 @@ public class SharesController : Controller
AuthorizeService = authorizeService;
}
[HttpGet("{serverId:int}/shares")]
public async Task<PagedData<ServerShareResponse>> GetAll(
[HttpGet]
public async Task<ActionResult<CountedData<ServerShareResponse>>> GetAllAsync(
[FromRoute] int serverId,
[FromQuery] [Range(0, int.MaxValue)] int page,
[FromQuery] [Range(1, 100)] int pageSize
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var server = await GetServerById(serverId);
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var query = ShareRepository
.Get()
.Where(x => x.Server.Id == server.Id);
.Where(x => x.Server.Id == server.Value.Id);
var count = await query.CountAsync();
var items = await query.Skip(page * pageSize).Take(pageSize).ToArrayAsync();
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.ToArrayAsync();
var userIds = items
.Select(x => x.UserId)
@@ -67,33 +78,33 @@ public class SharesController : Controller
{
Id = x.Id,
Username = users.First(y => y.Id == x.UserId).Username,
Permissions = x.Content.Permissions.ToArray()
Permissions = ShareMapper.MapToPermissionLevels(x.Content)
}).ToArray();
return new PagedData<ServerShareResponse>()
return new CountedData<ServerShareResponse>()
{
Items = mappedItems,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : count / pageSize
TotalCount = totalCount
};
}
[HttpGet("{serverId:int}/shares/{id:int}")]
public async Task<ServerShareResponse> Get(
[HttpGet("{id:int}")]
public async Task<ActionResult<ServerShareResponse>> GetAsync(
[FromRoute] int serverId,
[FromRoute] int id
)
{
var server = await GetServerById(serverId);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id);
.FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id);
if (share == null)
throw new HttpApiException("A share with that id cannot be found", 404);
return Problem("A share with that id cannot be found", statusCode: 404);
var user = await UserRepository
.Get()
@@ -103,127 +114,135 @@ public class SharesController : Controller
{
Id = share.Id,
Username = user.Username,
Permissions = share.Content.Permissions.ToArray()
Permissions = ShareMapper.MapToPermissionLevels(share.Content)
};
return mappedItem;
}
[HttpPost("{serverId:int}/shares")]
public async Task<ServerShareResponse> Create(
[HttpPost]
public async Task<ActionResult<ServerShareResponse>> CreateAsync(
[FromRoute] int serverId,
[FromBody] CreateShareRequest request
)
{
var server = await GetServerById(serverId);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Username == request.Username);
if (user == null)
throw new HttpApiException("A user with that username could not be found", 400);
return Problem("A user with that username could not be found", statusCode: 400);
var share = new ServerShare()
{
Server = server,
Content = new()
{
Permissions = request.Permissions
},
Server = server.Value,
Content = ShareMapper.MapToServerShareContent(request.Permissions),
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
UserId = user.Id
};
var finalShare = await ShareRepository.Add(share);
var finalShare = await ShareRepository.AddAsync(share);
var mappedItem = new ServerShareResponse()
{
Id = finalShare.Id,
Username = user.Username,
Permissions = finalShare.Content.Permissions.ToArray()
Permissions = ShareMapper.MapToPermissionLevels(finalShare.Content)
};
return mappedItem;
}
[HttpPatch("{serverId:int}/shares/{id:int}")]
public async Task<ServerShareResponse> Update(
[HttpPatch("{id:int}")]
public async Task<ActionResult<ServerShareResponse>> UpdateAsync(
[FromRoute] int serverId,
[FromRoute] int id,
[FromBody] UpdateShareRequest request
)
{
var server = await GetServerById(serverId);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id);
.FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id);
if (share == null)
throw new HttpApiException("A share with that id cannot be found", 404);
return Problem("A share with that id cannot be found", statusCode: 404);
share.Content = ShareMapper.MapToServerShareContent(request.Permissions);
share.Content.Permissions = request.Permissions;
share.UpdatedAt = DateTime.UtcNow;
await ShareRepository.Update(share);
await ShareRepository.UpdateAsync(share);
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == share.UserId);
if (user == null)
throw new HttpApiException("A user with that id could not be found", 400);
return Problem("A user with that id could not be found", statusCode: 400);
var mappedItem = new ServerShareResponse()
{
Id = share.Id,
Username = user.Username,
Permissions = share.Content.Permissions.ToArray()
Permissions = ShareMapper.MapToPermissionLevels(share.Content)
};
return mappedItem;
}
[HttpDelete("{serverId:int}/shares/{id:int}")]
public async Task Delete(
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteAsync(
[FromRoute] int serverId,
[FromRoute] int id
)
{
var server = await GetServerById(serverId);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id);
.FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id);
if (share == null)
throw new HttpApiException("A share with that id cannot be found", 404);
return Problem("A share with that id cannot be found", statusCode: 404);
await ShareRepository.Remove(share);
await ShareRepository.RemoveAsync(share);
return NoContent();
}
private async Task<Server> GetServerById(int serverId)
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.Authorize(
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
permission => permission is { Name: "shares", Type: >= ServerPermissionType.ReadWrite }
ServerPermissionConstants.Shares,
ServerPermissionLevel.ReadWrite
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
403
statusCode: 403
);
}

View File

@@ -1,11 +1,12 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Common;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Variables;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Variables;
@@ -14,29 +15,66 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers")]
[Route("api/client/servers/{serverId:int}/variables")]
public class VariablesController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<ServerVariable> ServerVariableRepository;
private readonly DatabaseRepository<StarVariable> StarVariableRepository;
private readonly ServerAuthorizeService AuthorizeService;
public VariablesController(
DatabaseRepository<Server> serverRepository,
ServerAuthorizeService authorizeService
ServerAuthorizeService authorizeService,
DatabaseRepository<ServerVariable> serverVariableRepository,
DatabaseRepository<StarVariable> starVariableRepository
)
{
ServerRepository = serverRepository;
AuthorizeService = authorizeService;
ServerVariableRepository = serverVariableRepository;
StarVariableRepository = starVariableRepository;
}
[HttpGet("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse[]> Get([FromRoute] int serverId)
[HttpGet]
public async Task<ActionResult<CountedData<ServerVariableDetailResponse>>> GetAsync(
[FromRoute] int serverId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var server = await GetServerById(serverId, ServerPermissionType.Read);
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
return server.Star.Variables.Select(starVariable =>
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.Read);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var query = StarVariableRepository
.Get()
.Where(x => x.Star.Id == server.Value.Star.Id);
var totalCount = await query.CountAsync();
var starVariables = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.ToArrayAsync();
var starVariableKeys = starVariables
.Select(x => x.Key)
.ToArray();
var serverVariables = await ServerVariableRepository
.Get()
.Where(x => x.Server.Id == server.Value.Id && starVariableKeys.Contains(x.Key))
.ToArrayAsync();
var responses = starVariables.Select(starVariable =>
{
var serverVariable = server.Variables.First(x => x.Key == starVariable.Key);
var serverVariable = serverVariables.First(x => x.Key == starVariable.Key);
return new ServerVariableDetailResponse()
{
@@ -48,17 +86,28 @@ public class VariablesController : Controller
Filter = starVariable.Filter
};
}).ToArray();
return new CountedData<ServerVariableDetailResponse>()
{
Items = responses,
TotalCount = totalCount
};
}
[HttpPut("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse> UpdateSingle(
[HttpPut]
public async Task<ActionResult<ServerVariableDetailResponse>> UpdateSingleAsync(
[FromRoute] int serverId,
[FromBody] UpdateServerVariableRequest request
)
{
// TODO: Handle filter
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var serverResult = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
var serverVariable = server.Variables.FirstOrDefault(x => x.Key == request.Key);
var starVariable = server.Star.Variables.FirstOrDefault(x => x.Key == request.Key);
@@ -67,7 +116,7 @@ public class VariablesController : Controller
throw new HttpApiException($"No variable with the key found: {request.Key}", 400);
serverVariable.Value = request.Value;
await ServerRepository.Update(server);
await ServerRepository.UpdateAsync(server);
return new ServerVariableDetailResponse()
{
@@ -80,13 +129,18 @@ public class VariablesController : Controller
};
}
[HttpPatch("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse[]> Update(
[HttpPatch]
public async Task<ActionResult<ServerVariableDetailResponse[]>> UpdateAsync(
[FromRoute] int serverId,
[FromBody] UpdateServerVariableRangeRequest request
)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var serverResult = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
foreach (var variable in request.Variables)
{
@@ -101,7 +155,7 @@ public class VariablesController : Controller
serverVariable.Value = variable.Value;
}
await ServerRepository.Update(server);
await ServerRepository.UpdateAsync(server);
return request.Variables.Select(requestVariable =>
{
@@ -120,28 +174,29 @@ public class VariablesController : Controller
}).ToArray();
}
private async Task<Server> GetServerById(int serverId, ServerPermissionType type)
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId, ServerPermissionLevel level)
{
var server = await ServerRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.Star)
.ThenInclude(x => x.Variables)
.Include(x => x.Variables)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.Authorize(
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
permission => permission.Name == "variables" && permission.Type >= type
ServerPermissionConstants.Variables,
level
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
403
statusCode: 403
);
}

View File

@@ -9,5 +9,5 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Remote.Nodes;
public class NodeTripController : Controller
{
[HttpGet("trip")]
public Task Get() => Task.CompletedTask;
public Task GetAsync() => Task.CompletedTask;
}

View File

@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MoonCore.Common;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
@@ -30,8 +31,14 @@ public class ServersController : Controller
}
[HttpGet]
public async Task<PagedData<ServerDataResponse>> Get([FromQuery] int page, [FromQuery] int pageSize)
public async Task<ActionResult<CountedData<ServerDataResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
// Load the node via the id
var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value);
@@ -51,8 +58,8 @@ public class ServersController : Controller
.ThenInclude(x => x.DockerImages)
.Include(x => x.Variables)
.Include(x => x.Allocations)
.Skip(page * pageSize)
.Take(pageSize)
.Skip(startIndex)
.Take(count)
.ToArrayAsync();
var serverData = new List<ServerDataResponse>();
@@ -67,18 +74,15 @@ public class ServersController : Controller
serverData.Add(convertedData);
}
return new PagedData<ServerDataResponse>()
return new CountedData<ServerDataResponse>()
{
Items = serverData.ToArray(),
CurrentPage = page,
PageSize = pageSize,
TotalItems = total,
TotalPages = total == 0 ? 0 : total / pageSize
TotalCount = total
};
}
[HttpGet("{id:int}")]
public async Task<ServerDataResponse> Get([FromRoute] int id)
public async Task<ServerDataResponse> GetAsync([FromRoute] int id)
{
// Load the node via the id
var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value);
@@ -110,7 +114,7 @@ public class ServersController : Controller
}
[HttpGet("{id:int}/install")]
public async Task<ServerInstallDataResponse> GetInstall([FromRoute] int id)
public async Task<ServerInstallDataResponse> GetInstallAsync([FromRoute] int id)
{
// Load the node via the id
var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value);
@@ -179,7 +183,6 @@ public class ServersController : Controller
Port = x.Port
}).ToArray(),
Variables = server.Variables.ToDictionary(x => x.Key, x => x.Value),
Bandwidth = server.Bandwidth,
Cpu = server.Cpu,
Disk = server.Disk,
Memory = server.Memory,
@@ -188,7 +191,6 @@ public class ServersController : Controller
PullDockerImage = dockerImage.AutoPulling,
ParseConiguration = server.Star.ParseConfiguration,
StopCommand = server.Star.StopCommand,
UseVirtualDisk = server.UseVirtualDisk
};
}
}

View File

@@ -1,10 +1,9 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Attributes;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
@@ -12,15 +11,18 @@ public class AdminAuthFilter : IServerAuthorizationFilter
{
private readonly IAuthorizationService AuthorizationService;
public int Priority => 0;
public AdminAuthFilter(IAuthorizationService authorizationService)
{
AuthorizationService = authorizationService;
}
public async Task<ServerAuthorizationResult?> Process(
public async Task<ServerAuthorizationResult?> ProcessAsync(
ClaimsPrincipal user,
Server server,
Func<ServerSharePermission, bool>? filter = null
string permissionId,
ServerPermissionLevel requiredLevel
)
{
var authResult = await AuthorizationService.AuthorizeAsync(

View File

@@ -1,24 +1,30 @@
using System.Security.Claims;
using MoonCore.Attributes;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
public class OwnerAuthFilter : IServerAuthorizationFilter
{
public Task<ServerAuthorizationResult?> Process(ClaimsPrincipal user, Server server, Func<ServerSharePermission, bool>? filter = null)
public int Priority => 0;
public Task<ServerAuthorizationResult?> ProcessAsync(
ClaimsPrincipal user,
Server server,
string permissionId,
ServerPermissionLevel requiredLevel
)
{
var userIdValue = user.FindFirstValue("userId");
var userIdValue = user.FindFirstValue("UserId");
if (string.IsNullOrEmpty(userIdValue)) // This is the case for api keys
return Task.FromResult<ServerAuthorizationResult?>(null);
var userId = int.Parse(userIdValue);
if(server.OwnerId != userId)
if (server.OwnerId != userId)
return Task.FromResult<ServerAuthorizationResult?>(null);
return Task.FromResult<ServerAuthorizationResult?>(

View File

@@ -1,11 +1,10 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using MoonCore.Attributes;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
@@ -18,10 +17,13 @@ public class ShareAuthFilter : IServerAuthorizationFilter
ShareRepository = shareRepository;
}
public async Task<ServerAuthorizationResult?> Process(
public int Priority => 0;
public async Task<ServerAuthorizationResult?> ProcessAsync(
ClaimsPrincipal user,
Server server,
Func<ServerSharePermission, bool>? filter = null
string permissionId,
ServerPermissionLevel requiredLevel
)
{
var userIdValue = user.FindFirstValue("userId");
@@ -38,10 +40,15 @@ public class ShareAuthFilter : IServerAuthorizationFilter
if (share == null)
return null;
if(filter == null)
if (string.IsNullOrEmpty(permissionId) || requiredLevel == ServerPermissionLevel.None)
return ServerAuthorizationResult.Success(share);
if(share.Content.Permissions.Any(filter))
var possiblePermShare = share.Content.Permissions.FirstOrDefault(x => x.Identifier == permissionId);
if (possiblePermShare == null)
return null;
if (possiblePermShare.Level >= requiredLevel)
return ServerAuthorizationResult.Success(share);
return null;

View File

@@ -1,17 +1,21 @@
using System.Security.Claims;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Interfaces;
public interface IServerAuthorizationFilter
{
// Return null => skip to next filter / handler
// Return any value, instant return
public Task<ServerAuthorizationResult?> Process(
// Return any value, instant complete
public int Priority { get; }
public Task<ServerAuthorizationResult?> ProcessAsync(
ClaimsPrincipal user,
Server server,
Func<ServerSharePermission, bool>? filter = null
string permissionId,
ServerPermissionLevel requiredLevel
);
}

View File

@@ -0,0 +1,18 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.NodeAllocations;
using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class AllocationMapper
{
public static partial NodeAllocationResponse ToNodeAllocation(Allocation allocation);
public static partial Allocation ToAllocation(CreateNodeAllocationRequest request);
public static partial void Merge(UpdateNodeAllocationRequest request, Allocation allocation);
// EF Projections
public static partial IQueryable<NodeAllocationResponse> ProjectToAdminResponse(this IQueryable<Allocation> allocations);
}

View File

@@ -0,0 +1,18 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.StarDockerImages;
using MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class DockerImageMapper
{
public static partial StarDockerImageResponse ToAdminResponse(StarDockerImage dockerImage);
public static partial StarDockerImage ToDockerImage(CreateStarDockerImageRequest request);
public static partial void Merge(UpdateStarDockerImageRequest request, StarDockerImage variable);
// EF Migrations
public static partial IQueryable<StarDockerImageResponse> ProjectToAdminResponse(this IQueryable<StarDockerImage> dockerImages);
}

View File

@@ -0,0 +1,18 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.Nodes;
using MoonlightServers.Shared.Http.Responses.Admin.Nodes;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class NodeMapper
{
public static partial NodeResponse ToAdminNodeResponse(Node node);
public static partial Node ToNode(CreateNodeRequest request);
public static partial void Merge(UpdateNodeRequest request, Node node);
// EF Projections
public static partial IQueryable<NodeResponse> ProjectToAdminResponse(this IQueryable<Node> nodes);
}

View File

@@ -0,0 +1,30 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.Servers;
using MoonlightServers.Shared.Http.Responses.Admin.Servers;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class ServerMapper
{
[UserMapping(Default = true)]
public static ServerResponse ToAdminServerResponse(Server server)
{
var response = ToAdminServerResponse_Internal(server);
response.AllocationIds = server.Allocations.Select(x => x.Id).ToArray();
return response;
}
private static partial ServerResponse ToAdminServerResponse_Internal(Server server);
[MapperIgnoreSource(nameof(CreateServerRequest.Variables))]
public static partial Server ToServer(CreateServerRequest request);
public static partial void Merge(UpdateServerRequest request, Server server);
// EF Projections
public static partial IQueryable<ServerResponse> ProjectToAdminResponse(this IQueryable<Server> servers);
}

View File

@@ -0,0 +1,15 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Responses.Admin.ServerVariables;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class ServerVariableMapper
{
public static partial ServerVariableResponse ToAdminResponse(ServerVariable serverVariable);
// EF Projections
public static partial IQueryable<ServerVariableResponse> ProjectToAdminResponse(this IQueryable<ServerVariable> variables);
}

View File

@@ -0,0 +1,27 @@
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Enums;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper]
public static partial class ShareMapper
{
public static ServerShareContent MapToServerShareContent(Dictionary<string, ServerPermissionLevel> permissionLevels)
{
return new ServerShareContent()
{
Permissions = permissionLevels.Select(x => new ServerShareContent.SharePermission()
{
Identifier = x.Key,
Level = x.Value
}).ToList()
};
}
public static Dictionary<string, ServerPermissionLevel> MapToPermissionLevels(
ServerShareContent content)
{
return content.Permissions.ToDictionary(x => x.Identifier, x => x.Level);
}
}

View File

@@ -0,0 +1,18 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.Stars;
using MoonlightServers.Shared.Http.Responses.Admin.Stars;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class StarMapper
{
public static partial StarResponse ToAdminResponse(Star star);
public static partial Star ToStar(CreateStarRequest request);
public static partial void Merge(UpdateStarRequest request, Star star);
// EF Projections
public static partial IQueryable<StarResponse> ProjectToAdminResponse(this IQueryable<Star> stars);
}

View File

@@ -0,0 +1,18 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.StarVariables;
using MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class StarVariableMapper
{
public static partial StarVariableResponse ToAdminResponse(StarVariable variable);
public static partial StarVariable ToStarVariable(CreateStarVariableRequest request);
public static partial void Merge(UpdateStarVariableRequest request, StarVariable variable);
// EF Projections
public static partial IQueryable<StarVariableResponse> ProjectToAdminResponse(this IQueryable<StarVariable> variables);
}

View File

@@ -1,8 +1,14 @@
using MoonlightServers.Shared.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Models;
public class ServerShareContent
public record ServerShareContent
{
public List<ServerSharePermission> Permissions { get; set; } = [];
public List<SharePermission> Permissions { get; set; } = new();
public record SharePermission
{
public string Identifier { get; set; }
public ServerPermissionLevel Level { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@@ -11,37 +11,26 @@
<PackageId>MoonlightServers.ApiServer</PackageId>
<Title>MoonlightServers.ApiServer</Title>
<Version>2.1.0</Version>
<PackageTags>apiserver</PackageTags>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moonlight.ApiServer" Version="2.1.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5"/>
<PackageReference Include="Moonlight.ApiServer" Version="2.1.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj"/>
<ProjectReference Include="..\MoonlightServers.Frontend\MoonlightServers.Frontend.csproj"/>
<ProjectReference Include="..\MoonlightServers.Shared\MoonlightServers.Shared.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Migrations\"/>
<Folder Include="Http\Middleware\"/>
<Folder Include="Startup\" />
</ItemGroup>
<ItemGroup Label="Build instruction for nuget package building">
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="**\*.razor" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*"/>
<Content Remove="storage\**\*"/>
<None Remove="storage\**\*"/>

View File

@@ -1,3 +1,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Extensions;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Models;
@@ -7,12 +10,11 @@ using MoonlightServers.ApiServer.Helpers;
using MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
using MoonlightServers.ApiServer.Interfaces;
namespace MoonlightServers.ApiServer.Startup;
namespace MoonlightServers.ApiServer;
[PluginStartup]
public class PluginStartup : IPluginStartup
{
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
public void AddPlugin(WebApplicationBuilder builder)
{
// Scan the current plugin assembly for di services
builder.Services.AutoAddServices<PluginStartup>();
@@ -24,19 +26,20 @@ public class PluginStartup : IPluginStartup
.AddAuthentication()
.AddScheme<NodeAuthOptions, NodeAuthScheme>("nodeAuthentication", null);
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
var configuration = AppConfiguration.CreateEmpty();
builder.Configuration.Bind(configuration);
if (configuration.Client.Enable)
if (configuration.Frontend.EnableHosting)
{
builder.Services.AddSingleton(new FrontendConfigurationOption()
{
Scripts =
[
"js/XtermBlazor.min.js",
"js/addon-fit.js",
"js/moonlightServers.js"
"/_content/MoonlightServers.Frontend/js/XtermBlazor.min.js",
"/_content/MoonlightServers.Frontend/js/addon-fit.js",
"/_content/MoonlightServers.Frontend/js/moonlightServers.js"
],
Styles = ["css/XtermBlazor.min.css"]
Styles = ["/_content/MoonlightServers.Frontend/css/XtermBlazor.min.css"]
});
}
@@ -44,13 +47,13 @@ public class PluginStartup : IPluginStartup
builder.Services.AddSingleton<IServerAuthorizationFilter, OwnerAuthFilter>();
builder.Services.AddScoped<IServerAuthorizationFilter, AdminAuthFilter>();
builder.Services.AddScoped<IServerAuthorizationFilter, ShareAuthFilter>();
return Task.CompletedTask;
}
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app)
=> Task.CompletedTask;
public void UsePlugin(WebApplication app)
{
}
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder)
=> Task.CompletedTask;
public void MapPlugin(WebApplication app)
{
}
}

View File

@@ -1,18 +0,0 @@
using System.Text.Json;
using Moonlight.ApiServer;
using Moonlight.ApiServer.Models;
using MoonlightServers.ApiServer.Startup;
// Development Server Startup
// This file is a small helper for development instances for moonlight.
// It calls the moonlight startup with the current project loaded as a plugin.
// This allows you to develop and debug projects without any hassle
// !!! DO NOT HARDCORE ANY SECRETS HERE !!!
var startup = new Startup();
await startup.Run(args, [
new PluginStartup()
]);

View File

@@ -1,28 +0,0 @@
namespace MoonlightServers.ApiServer.Services;
public class NodeBootService : IHostedLifecycleService
{
public async Task StartedAsync(CancellationToken cancellationToken)
{
// TODO: Add node boot calls here
}
#region Unused
public Task StartAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StartingAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
#endregion
}

View File

@@ -1,9 +1,7 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Attributes;
using MoonCore.Extended.Helpers;
using MoonCore.Helpers;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
@@ -41,7 +39,7 @@ public class NodeService
return jwtSecurityTokenHandler.WriteToken(securityToken);
}
public async Task<SystemStatusResponse> GetSystemStatus(Node node)
public async Task<SystemStatusResponse> GetSystemStatusAsync(Node node)
{
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<SystemStatusResponse>("api/system/status");
@@ -49,13 +47,13 @@ public class NodeService
#region Statistics
public async Task<StatisticsResponse> GetStatistics(Node node)
public async Task<StatisticsResponse> GetStatisticsAsync(Node node)
{
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsResponse>("api/statistics");
}
public async Task<StatisticsDockerResponse> GetDockerStatistics(Node node)
public async Task<StatisticsDockerResponse> GetDockerStatisticsAsync(Node node)
{
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsDockerResponse>("api/statistics/docker");

View File

@@ -3,31 +3,37 @@ using MoonCore.Attributes;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Services;
[Scoped]
public class ServerAuthorizeService
{
private readonly IEnumerable<IServerAuthorizationFilter> AuthorizationFilters;
private readonly IServerAuthorizationFilter[] AuthorizationFilters;
public ServerAuthorizeService(
IEnumerable<IServerAuthorizationFilter> authorizationFilters
)
{
AuthorizationFilters = authorizationFilters;
AuthorizationFilters = authorizationFilters.ToArray();
}
public async Task<ServerAuthorizationResult> Authorize(
public async Task<ServerAuthorizationResult> AuthorizeAsync(
ClaimsPrincipal user,
Server server,
Func<ServerSharePermission, bool>? filter = null
string permissionIdentifier,
ServerPermissionLevel permissionLevel
)
{
foreach (var authorizationFilter in AuthorizationFilters)
{
var result = await authorizationFilter.Process(user, server, filter);
var result = await authorizationFilter.ProcessAsync(
user,
server,
permissionIdentifier,
permissionLevel
);
if (result != null)
return result;

View File

@@ -24,45 +24,54 @@ public class ServerFileSystemService
ServerRepository = serverRepository;
}
public async Task<ServerFileSystemResponse[]> List(Server server, string path)
public async Task<ServerFileSystemResponse[]> ListAsync(Server server, string path)
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
return await apiClient.GetJson<ServerFileSystemResponse[]>(
$"api/servers/{server.Id}/files/list?path={path}"
);
}
public async Task Move(Server server, string oldPath, string newPath)
public async Task MoveAsync(Server server, string oldPath, string newPath)
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/move?oldPath={oldPath}&newPath={newPath}"
);
}
public async Task Delete(Server server, string path)
public async Task DeleteAsync(Server server, string path)
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Delete(
$"api/servers/{server.Id}/files/delete?path={path}"
);
}
public async Task Mkdir(Server server, string path)
public async Task MkdirAsync(Server server, string path)
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/mkdir?path={path}"
);
}
public async Task Compress(Server server, CompressType type, string[] items, string destination)
public async Task TouchAsync(Server server, string path)
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/touch?path={path}"
);
}
public async Task CompressAsync(Server server, CompressType type, string[] items, string destination)
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/compress",
@@ -75,9 +84,9 @@ public class ServerFileSystemService
);
}
public async Task Decompress(Server server, CompressType type, string path, string destination)
public async Task DecompressAsync(Server server, CompressType type, string path, string destination)
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/decompress",
@@ -92,7 +101,7 @@ public class ServerFileSystemService
#region Helpers
private async Task<HttpApiClient> GetApiClient(Server server)
private async Task<HttpApiClient> GetApiClientAsync(Server server)
{
var serverWithNode = server;

View File

@@ -1,4 +1,3 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using MoonCore.Attributes;
using MoonCore.Exceptions;
@@ -6,6 +5,7 @@ using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
namespace MoonlightServers.ApiServer.Services;
@@ -24,11 +24,11 @@ public class ServerService
#region Power Actions
public async Task Start(Server server)
public async Task StartAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/start");
}
catch (HttpRequestException e)
@@ -37,11 +37,11 @@ public class ServerService
}
}
public async Task Stop(Server server)
public async Task StopAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/stop");
}
catch (HttpRequestException e)
@@ -50,11 +50,11 @@ public class ServerService
}
}
public async Task Kill(Server server)
public async Task KillAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/kill");
}
catch (HttpRequestException e)
@@ -65,11 +65,11 @@ public class ServerService
#endregion
public async Task Install(Server server)
public async Task InstallAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/install");
}
catch (HttpRequestException e)
@@ -78,11 +78,11 @@ public class ServerService
}
}
public async Task Sync(Server server)
public async Task SyncAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/sync");
}
catch (HttpRequestException e)
@@ -91,11 +91,11 @@ public class ServerService
}
}
public async Task SyncDelete(Server server)
public async Task SyncDeleteAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
await apiClient.Delete($"api/servers/{server.Id}");
}
catch (HttpRequestException e)
@@ -104,11 +104,11 @@ public class ServerService
}
}
public async Task<ServerStatusResponse> GetStatus(Server server)
public async Task<ServerStatusResponse> GetStatusAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
return await apiClient.GetJson<ServerStatusResponse>($"api/servers/{server.Id}/status");
}
catch (HttpRequestException e)
@@ -117,11 +117,11 @@ public class ServerService
}
}
public async Task<ServerLogsResponse> GetLogs(Server server)
public async Task<ServerLogsResponse> GetLogsAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
return await apiClient.GetJson<ServerLogsResponse>($"api/servers/{server.Id}/logs");
}
catch (HttpRequestException e)
@@ -130,14 +130,34 @@ public class ServerService
}
}
public async Task<ServerStatsResponse> GetStats(Server server)
public async Task<ServerStatsResponse> GetStatsAsync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
using var apiClient = await GetApiClientAsync(server);
return await apiClient.GetJson<ServerStatsResponse>($"api/servers/{server.Id}/stats");
}
catch (HttpRequestException e)
catch (HttpRequestException)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task RunCommandAsync(Server server, string command)
{
try
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/command",
new ServerCommandRequest()
{
Command = command
}
);
}
catch (HttpRequestException)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
@@ -150,14 +170,10 @@ public class ServerService
if (server.OwnerId == user.Id)
return true;
var permissions = JsonSerializer.Deserialize<string[]>(
user.PermissionsJson
) ?? [];
return PermissionHelper.HasPermission(permissions, "admin.servers.get");
return PermissionHelper.HasPermission(user.Permissions, "admin.servers.get");
}
private async Task<HttpApiClient> GetApiClient(Server server)
private async Task<HttpApiClient> GetApiClientAsync(Server server)
{
var serverWithNode = server;

View File

@@ -1,5 +1,6 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
@@ -22,7 +23,7 @@ public class StarImportExportService
Logger = logger;
}
public async Task<string> Export(int id)
public async Task<string> ExportAsync(int id)
{
var star = StarRepository
.Get()
@@ -77,20 +78,20 @@ public class StarImportExportService
return json;
}
public async Task<Star> Import(string json)
public async Task<Star> ImportAsync(string json)
{
// Determine which importer to use based on simple patterns
if (json.Contains("RequiredAllocations"))
return await ImportStar(json);
return await ImportStarAsync(json);
else if (json.Contains("AllocationsNeeded"))
return await ImportImage(json);
return await ImportImageAsync(json);
else if (json.Contains("_comment"))
return await ImportEgg(json);
return await ImportEggAsync(json);
else
throw new HttpApiException("Unable to determine the format of the imported star/image/egg", 400);
}
public async Task<Star> ImportStar(string json)
public async Task<Star> ImportStarAsync(string json)
{
try
{
@@ -137,7 +138,7 @@ public class StarImportExportService
}).ToList()
};
var finalStar = await StarRepository.Add(star);
var finalStar = await StarRepository.AddAsync(star);
return finalStar;
}
@@ -148,7 +149,7 @@ public class StarImportExportService
}
}
public async Task<Star> ImportImage(string json)
public async Task<Star> ImportImageAsync(string json)
{
try
{
@@ -234,7 +235,7 @@ public class StarImportExportService
#endregion
var finalStar = await StarRepository.Add(star);
var finalStar = await StarRepository.AddAsync(star);
return finalStar;
}
@@ -245,7 +246,7 @@ public class StarImportExportService
}
}
public async Task<Star> ImportEgg(string json)
public async Task<Star> ImportEggAsync(string json)
{
// Create result
var star = new Star();
@@ -402,7 +403,7 @@ public class StarImportExportService
star.AllowDockerImageChange = true;
// Finally save it to the db
var finalStar = await StarRepository.Add(star);
var finalStar = await StarRepository.AddAsync(star);
return finalStar;
}

View File

@@ -1,5 +1,3 @@
using MoonCore.Helpers;
namespace MoonlightServers.Daemon.Configuration;
public class AppConfiguration
@@ -44,10 +42,10 @@ public class AppConfiguration
public class StorageData
{
public string Volumes { get; set; } = PathBuilder.Dir("volumes");
public string VirtualDisks { get; set; } = PathBuilder.Dir("virtualDisks");
public string Backups { get; set; } = PathBuilder.Dir("backups");
public string Install { get; set; } = PathBuilder.Dir("install");
public string Volumes { get; set; } = Path.Combine("storage", "volumes");
public string VirtualDisks { get; set; } = Path.Combine("storage", "virtualDisks");
public string Backups { get; set; } = Path.Combine("storage", "backups");
public string Install { get; set; } =Path.Combine("storage", "install");
public VirtualDiskData VirtualDiskOptions { get; set; } = new();
}

View File

@@ -23,8 +23,6 @@ public static class ServerConfigurationExtensions
Variables = response.Variables,
OnlineDetection = response.OnlineDetection,
DockerImage = response.DockerImage,
UseVirtualDisk = response.UseVirtualDisk,
Bandwidth = response.Bandwidth,
Cpu = response.Cpu,
Disk = response.Disk,
Memory = response.Memory,

View File

@@ -0,0 +1,31 @@
namespace MoonlightServers.Daemon.Helpers;
public class CompositeServiceProvider : IServiceProvider
{
private readonly List<IServiceProvider> ServiceProviders;
public CompositeServiceProvider(params IServiceProvider[] serviceProviders)
{
ServiceProviders = new List<IServiceProvider>(serviceProviders);
}
public object? GetService(Type serviceType)
{
foreach (var provider in ServiceProviders)
{
try
{
var service = provider.GetService(serviceType);
if (service != null)
return service;
}
catch (InvalidOperationException)
{
// Ignored
}
}
return null;
}
}

View File

@@ -57,18 +57,18 @@ public class HostSystemHelper
#region CPU Usage
public async Task<CpuUsageDetails> GetCpuUsage()
public async Task<CpuUsageDetails> GetCpuUsageAsync()
{
var result = new CpuUsageDetails();
var perCoreUsages = new List<double>();
// Initial read
var (cpuLastStats, cpuLastSums) = await ReadAllCpuStats();
var (cpuLastStats, cpuLastSums) = await ReadAllCpuStatsAsync();
await Task.Delay(1000);
// Second read
var (cpuNowStats, cpuNowSums) = await ReadAllCpuStats();
var (cpuNowStats, cpuNowSums) = await ReadAllCpuStatsAsync();
for (var i = 0; i < cpuNowStats.Length; i++)
{
@@ -94,7 +94,7 @@ public class HostSystemHelper
return result;
}
private async Task<(long[][] cpuStatsList, long[] cpuSums)> ReadAllCpuStats()
private async Task<(long[][] cpuStatsList, long[] cpuSums)> ReadAllCpuStatsAsync()
{
var lines = await File.ReadAllLinesAsync("/proc/stat");
@@ -128,12 +128,12 @@ public class HostSystemHelper
#region Memory
public async Task ClearCachedMemory()
public async Task ClearCachedMemoryAsync()
{
await File.WriteAllTextAsync("/proc/sys/vm/drop_caches", "3");
}
public async Task<MemoryUsageDetails> GetMemoryUsage()
public async Task<MemoryUsageDetails> GetMemoryUsageAsync()
{
var details = new MemoryUsageDetails();
@@ -194,7 +194,7 @@ public class HostSystemHelper
#region Disks
public async Task<DiskUsageDetails[]> GetDiskUsages()
public async Task<DiskUsageDetails[]> GetDiskUsagesAsync()
{
var details = new List<DiskUsageDetails>();

View File

@@ -3,7 +3,6 @@ using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
using Mono.Unix.Native;
using MoonCore.Unix.Exceptions;
using MoonCore.Unix.SecureFs;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
@@ -19,7 +18,7 @@ public class ServerFileSystem
FileSystem = fileSystem;
}
public Task<ServerFileSystemResponse[]> List(string inputPath)
public Task<ServerFileSystemResponse[]> ListAsync(string inputPath)
{
var path = Normalize(inputPath);
var entries = FileSystem.ReadDir(path);
@@ -35,7 +34,7 @@ public class ServerFileSystem
.Select(x => new ServerFileSystemResponse()
{
Name = x.Name,
IsFile = x.IsFile,
IsFolder = x.IsDirectory,
Size = x.Size,
UpdatedAt = x.LastChanged,
CreatedAt = x.CreatedAt
@@ -45,7 +44,7 @@ public class ServerFileSystem
return Task.FromResult(result);
}
public Task Move(string inputOldPath, string inputNewPath)
public Task MoveAsync(string inputOldPath, string inputNewPath)
{
var oldPath = Normalize(inputOldPath);
var newPath = Normalize(inputNewPath);
@@ -55,7 +54,7 @@ public class ServerFileSystem
return Task.CompletedTask;
}
public Task Delete(string inputPath)
public Task DeleteAsync(string inputPath)
{
var path = Normalize(inputPath);
@@ -64,7 +63,7 @@ public class ServerFileSystem
return Task.CompletedTask;
}
public Task Mkdir(string inputPath)
public Task MkdirAsync(string inputPath)
{
var path = Normalize(inputPath);
@@ -73,7 +72,25 @@ public class ServerFileSystem
return Task.CompletedTask;
}
public Task CreateChunk(string inputPath, long totalSize, long positionToSkip, Stream chunkStream)
public Task TouchAsync(string inputPath)
{
var path = Normalize(inputPath);
var parentDirectory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(
path,
_ => { },
OpenFlags.O_CREAT
); // We use these custom flags to ensure we aren't overwriting the file
return Task.CompletedTask;
}
public Task CreateChunkAsync(string inputPath, long totalSize, long positionToSkip, Stream chunkStream)
{
var path = Normalize(inputPath);
@@ -96,7 +113,7 @@ public class ServerFileSystem
return Task.CompletedTask;
}
public Task Create(string inputPath, Stream dataStream)
public Task CreateAsync(string inputPath, Stream dataStream)
{
var path = Normalize(inputPath);
@@ -116,7 +133,7 @@ public class ServerFileSystem
return Task.CompletedTask;
}
public Task Read(string inputPath, Func<Stream, Task> onHandle)
public Task ReadAsync(string inputPath, Func<Stream, Task> onHandle)
{
var path = Normalize(inputPath);
@@ -131,7 +148,7 @@ public class ServerFileSystem
#region Compression
public Task Compress(string[] itemsInput, string destinationInput, CompressType type)
public Task CompressAsync(string[] itemsInput, string destinationInput, CompressType type)
{
var destination = Normalize(destinationInput);
var items = itemsInput.Select(Normalize);
@@ -173,7 +190,7 @@ public class ServerFileSystem
return Task.CompletedTask;
}
public Task Decompress(string pathInput, string destinationInput, CompressType type)
public Task DecompressAsync(string pathInput, string destinationInput, CompressType type)
{
var path = Normalize(pathInput);
var destination = Normalize(destinationInput);
@@ -259,10 +276,7 @@ public class ServerFileSystem
outputStream.PutNextEntry(entry);
FileSystem.OpenFileRead(path, stream =>
{
stream.CopyTo(outputStream);
});
FileSystem.OpenFileRead(path, stream => { stream.CopyTo(outputStream); });
outputStream.CloseEntry();
}
@@ -274,10 +288,10 @@ public class ServerFileSystem
{
var entry = inputStream.GetNextEntry();
if(entry == null)
if (entry == null)
break;
if(entry.IsDirectory)
if (entry.IsDirectory)
continue;
var fileDestination = Path.Combine(destination, entry.Name);
@@ -304,10 +318,10 @@ public class ServerFileSystem
{
var entry = inputStream.GetNextEntry();
if(entry == null)
if (entry == null)
break;
if(entry.IsDirectory)
if (entry.IsDirectory)
continue;
var fileDestination = Path.Combine(destination, entry.Name);

View File

@@ -17,7 +17,7 @@ public class UnsafeDockerClient
Configuration = configuration;
}
public Task<HttpClient> CreateHttpClient()
public Task<HttpClient> CreateHttpClientAsync()
{
var client = new HttpClient(new SocketsHttpHandler()
{
@@ -35,9 +35,9 @@ public class UnsafeDockerClient
return Task.FromResult(client);
}
public async Task<DataUsageResponse> GetDataUsage()
public async Task<DataUsageResponse> GetDataUsageAsync()
{
using var client = await CreateHttpClient();
using var client = await CreateHttpClientAsync();
var responseJson = await client.GetStringAsync("http://some.random.domain/v1.47/system/df");
var response = JsonSerializer.Deserialize<DataUsageResponse>(responseJson)!;

View File

@@ -1,44 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.ServerSystem.SubSystems;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/download")]
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverDownload")]
public class DownloadController : Controller
{
private readonly ServerService ServerService;
public DownloadController(ServerService serverService)
{
ServerService = serverService;
}
[HttpGet]
public async Task Download()
{
var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value);
var path = User.Claims.First(x => x.Type == "path").Value;
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var storageSubSystem = server.GetRequiredSubSystem<StorageSubSystem>();
var fileSystem = await storageSubSystem.GetFileSystem();
await fileSystem.Read(
path,
async dataStream =>
{
await Results.File(dataStream).ExecuteAsync(HttpContext);
}
);
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/{id:int}")]
public class PowerController : Controller
{
private readonly ServerService ServerService;
public PowerController(ServerService serverService)
{
ServerService = serverService;
}
[HttpPost("start")]
public async Task<ActionResult> StartAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Start))
return Problem("Cannot fire start trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Start);
return NoContent();
}
[HttpPost("stop")]
public async Task<ActionResult> StopAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Stop))
return Problem("Cannot fire stop trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Stop);
return NoContent();
}
[HttpPost("kill")]
public async Task<ActionResult> KillAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Kill))
return Problem("Cannot fire kill trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Kill);
return NoContent();
}
[HttpPost("install")]
public async Task<ActionResult> InstallAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Install))
return Problem("Cannot fire install trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Install);
return NoContent();
}
}

View File

@@ -1,91 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.ServerSystem.SubSystems;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[Authorize]
[ApiController]
[Route("api/servers")]
public class ServerFileSystemController : Controller
{
private readonly ServerService ServerService;
public ServerFileSystemController(ServerService serverService)
{
ServerService = serverService;
}
[HttpGet("{id:int}/files/list")]
public async Task<ServerFileSystemResponse[]> List([FromRoute] int id, [FromQuery] string path = "")
{
var fileSystem = await GetFileSystemById(id);
return await fileSystem.List(path);
}
[HttpPost("{id:int}/files/move")]
public async Task Move([FromRoute] int id, [FromQuery] string oldPath, [FromQuery] string newPath)
{
var fileSystem = await GetFileSystemById(id);
await fileSystem.Move(oldPath, newPath);
}
[HttpDelete("{id:int}/files/delete")]
public async Task Delete([FromRoute] int id, [FromQuery] string path)
{
var fileSystem = await GetFileSystemById(id);
await fileSystem.Delete(path);
}
[HttpPost("{id:int}/files/mkdir")]
public async Task Mkdir([FromRoute] int id, [FromQuery] string path)
{
var fileSystem = await GetFileSystemById(id);
await fileSystem.Mkdir(path);
}
[HttpPost("{id:int}/files/compress")]
public async Task Compress([FromRoute] int id, [FromBody] ServerFilesCompressRequest request)
{
var fileSystem = await GetFileSystemById(id);
await fileSystem.Compress(
request.Items,
request.Destination,
request.Type
);
}
[HttpPost("{id:int}/files/decompress")]
public async Task Decompress([FromRoute] int id, [FromBody] ServerFilesDecompressRequest request)
{
var fileSystem = await GetFileSystemById(id);
await fileSystem.Decompress(
request.Path,
request.Destination,
request.Type
);
}
private async Task<ServerFileSystem> GetFileSystemById(int serverId)
{
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var storageSubSystem = server.GetRequiredSubSystem<StorageSubSystem>();
return await storageSubSystem.GetFileSystem();
}
}

View File

@@ -1,65 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Services;
using ServerTrigger = MoonlightServers.Daemon.ServerSystem.ServerTrigger;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[Authorize]
[ApiController]
[Route("api/servers")]
public class ServerPowerController : Controller
{
private readonly ServerService ServerService;
public ServerPowerController(ServerService serverService)
{
ServerService = serverService;
}
[HttpPost("{serverId:int}/start")]
public async Task Start(int serverId)
{
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
await server.Trigger(ServerTrigger.Start);
}
[HttpPost("{serverId:int}/stop")]
public async Task Stop(int serverId)
{
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
await server.Trigger(ServerTrigger.Stop);
}
[HttpPost("{serverId:int}/install")]
public async Task Install(int serverId)
{
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
await server.Trigger(ServerTrigger.Install);
}
[HttpPost("{serverId:int}/kill")]
public async Task Kill(int serverId)
{
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
await server.Trigger(ServerTrigger.Kill);
}
}

View File

@@ -1,88 +1,67 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.ServerSystem.SubSystems;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[Authorize]
[ApiController]
[Route("api/servers")]
[Route("api/servers/{id:int}")]
public class ServersController : Controller
{
private readonly ServerService ServerService;
private readonly ServerConfigurationMapper ConfigurationMapper;
public ServersController(ServerService serverService)
public ServersController(ServerService serverService, ServerConfigurationMapper configurationMapper)
{
ServerService = serverService;
ConfigurationMapper = configurationMapper;
}
[HttpPost("{serverId:int}/sync")]
public async Task Sync([FromRoute] int serverId)
[HttpPost("sync")]
public async Task<ActionResult> SyncAsync([FromRoute] int id)
{
await ServerService.Sync(serverId);
await ServerService.InitializeByIdAsync(id);
return NoContent();
}
[HttpDelete("{serverId:int}")]
public async Task Delete([FromRoute] int serverId)
[HttpGet("status")]
public async Task<ActionResult<ServerStatusResponse>> StatusAsync([FromRoute] int id)
{
await ServerService.Delete(serverId);
}
[HttpGet("{serverId:int}/status")]
public Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
{
var server = ServerService.Find(serverId);
var server = ServerService.GetById(id);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var result = new ServerStatusResponse()
return new ServerStatusResponse()
{
State = (ServerState)server.StateMachine.State
};
return Task.FromResult(result);
}
[HttpGet("{serverId:int}/logs")]
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
[HttpGet("logs")]
public async Task<ActionResult<ServerLogsResponse>> LogsAsync([FromRoute] int id)
{
var server = ServerService.Find(serverId);
var server = ServerService.GetById(id);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return Problem("No server with this id found", statusCode: 404);
var consoleSubSystem = server.GetRequiredSubSystem<ConsoleSubSystem>();
var messages = await consoleSubSystem.RetrieveCache();
var messages = await server.Console.GetCacheAsync();
return new ServerLogsResponse()
{
Messages = messages
Messages = messages.ToArray()
};
}
[HttpGet("{serverId:int}/stats")]
public Task<ServerStatsResponse> GetStats([FromRoute] int serverId)
[HttpGet("stats")]
public async Task<ServerStatsResponse> GetStatsAsync([FromRoute] int id)
{
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var statsSubSystem = server.GetRequiredSubSystem<StatsSubSystem>();
return Task.FromResult<ServerStatsResponse>(new()
return new ServerStatsResponse()
{
CpuUsage = statsSubSystem.CurrentStats.CpuUsage,
MemoryUsage = statsSubSystem.CurrentStats.MemoryUsage,
NetworkRead = statsSubSystem.CurrentStats.NetworkRead,
NetworkWrite = statsSubSystem.CurrentStats.NetworkWrite,
IoRead = statsSubSystem.CurrentStats.IoRead,
IoWrite = statsSubSystem.CurrentStats.IoWrite
});
};
}
}

View File

@@ -1,85 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.ServerSystem.SubSystems;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/upload")]
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverUpload")]
public class UploadController : Controller
{
private readonly AppConfiguration Configuration;
private readonly ServerService ServerService;
public UploadController(
ServerService serverService,
AppConfiguration configuration
)
{
ServerService = serverService;
Configuration = configuration;
}
[HttpPost]
public async Task Upload(
[FromQuery] long totalSize,
[FromQuery] int chunkId,
[FromQuery] string path
)
{
var chunkSize = ByteConverter.FromMegaBytes(Configuration.Files.UploadChunkSize).Bytes;
var uploadLimit = ByteConverter.FromMegaBytes(Configuration.Files.UploadSizeLimit).Bytes;
#region File validation
if (Request.Form.Files.Count != 1)
throw new HttpApiException("You need to provide exactly one file", 400);
var file = Request.Form.Files[0];
if (file.Length > chunkSize)
throw new HttpApiException("The provided data exceeds the chunk size limit", 400);
#endregion
var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value);
#region Chunk calculation and validation
if(totalSize > uploadLimit)
throw new HttpApiException("Invalid upload request: Exceeding upload limit", 400);
var chunks = totalSize / chunkSize;
chunks += totalSize % chunkSize > 0 ? 1 : 0;
if (chunkId > chunks)
throw new HttpApiException("Invalid chunk id: Out of bounds", 400);
var positionToSkipTo = chunkSize * chunkId;
#endregion
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var storageSubSystem = server.GetRequiredSubSystem<StorageSubSystem>();
var fileSystem = await storageSubSystem.GetFileSystem();
var dataStream = file.OpenReadStream();
await fileSystem.CreateChunk(
path,
totalSize,
positionToSkipTo,
dataStream
);
}
}

View File

@@ -18,17 +18,17 @@ public class StatisticsController : Controller
}
[HttpGet]
public async Task<StatisticsResponse> Get()
public async Task<StatisticsResponse> GetAsync()
{
var response = new StatisticsResponse();
var cpuUsage = await HostSystemHelper.GetCpuUsage();
var cpuUsage = await HostSystemHelper.GetCpuUsageAsync();
response.Cpu.Model = cpuUsage.Model;
response.Cpu.Usage = cpuUsage.OverallUsage;
response.Cpu.UsagePerCore = cpuUsage.PerCoreUsage;
var memoryUsage = await HostSystemHelper.GetMemoryUsage();
var memoryUsage = await HostSystemHelper.GetMemoryUsageAsync();
response.Memory.Available = memoryUsage.Available;
response.Memory.Cached = memoryUsage.Cached;
@@ -37,7 +37,7 @@ public class StatisticsController : Controller
response.Memory.SwapTotal = memoryUsage.SwapTotal;
response.Memory.SwapFree = memoryUsage.SwapFree;
var diskDetails = await HostSystemHelper.GetDiskUsages();
var diskDetails = await HostSystemHelper.GetDiskUsagesAsync();
response.Disks = diskDetails.Select(x => new StatisticsResponse.DiskData()
{

View File

@@ -20,13 +20,13 @@ public class StatisticsDockerController : Controller
}
[HttpGet]
public async Task<StatisticsDockerResponse> Get()
public async Task<StatisticsDockerResponse> GetAsync()
{
var usage = await DockerInfoService.GetDataUsage();
var usage = await DockerInfoService.GetDataUsageAsync();
return new StatisticsDockerResponse
{
Version = await DockerInfoService.GetDockerVersion(),
Version = await DockerInfoService.GetDockerVersionAsync(),
ContainersReclaimable = usage.Containers.Reclaimable,
ContainersUsed = usage.Containers.Used,
BuildCacheReclaimable = usage.BuildCache.Reclaimable,

View File

@@ -18,7 +18,7 @@ public class SystemStatusController : Controller
RemoteService = remoteService;
}
public async Task<SystemStatusResponse> Get()
public async Task<SystemStatusResponse> GetAsync()
{
SystemStatusResponse response;
@@ -27,7 +27,7 @@ public class SystemStatusController : Controller
try
{
await RemoteService.GetStatus();
await RemoteService.GetStatusAsync();
sw.Stop();

View File

@@ -0,0 +1,363 @@
using Docker.DotNet.Models;
using Mono.Unix.Native;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Mappers;
public class ServerConfigurationMapper
{
private readonly AppConfiguration AppConfiguration;
public ServerConfigurationMapper(AppConfiguration appConfiguration)
{
AppConfiguration = appConfiguration;
}
public ServerConfiguration FromServerDataResponse(ServerDataResponse response)
{
return new ServerConfiguration()
{
Id = response.Id,
StartupCommand = response.StartupCommand,
Allocations = response.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
{
IpAddress = y.IpAddress,
Port = y.Port
}).ToArray(),
Variables = response.Variables,
OnlineDetection = response.OnlineDetection,
DockerImage = response.DockerImage,
Cpu = response.Cpu,
Disk = response.Disk,
Memory = response.Memory,
StopCommand = response.StopCommand,
};
}
public CreateContainerParameters ToRuntimeParameters(
ServerConfiguration serverConfiguration,
string hostPath,
string containerName
)
{
var parameters = ToSharedParameters(serverConfiguration);
#region Security
parameters.HostConfig.CapDrop = new List<string>()
{
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap"
};
parameters.HostConfig.ReadonlyRootfs = true;
parameters.HostConfig.SecurityOpt = new List<string>()
{
"no-new-privileges"
};
#endregion
#region Name
parameters.Name = containerName;
parameters.Hostname = containerName;
#endregion
#region Docker Image
parameters.Image = serverConfiguration.DockerImage;
#endregion
#region Working Dir
parameters.WorkingDir = "/home/container";
#endregion
#region User
// TODO: Extract this to an external service with config options and return a userspace user id and a install user id
// in order to know which permissions are required in order to run the container with the correct permissions
var userId = Syscall.getuid();
if (userId == 0)
userId = 998;
parameters.User = $"{userId}:{userId}";
/*
if (userId == 0)
{
// We are running as root, so we need to run the container as another user and chown the files when we make changes
parameters.User = $"998:998";
}
else
{
// We are not running as root, so we start the container as the same user,
// as we are not able to chown the container content to a different user
parameters.User = $"{userId}:{userId}";
}*/
#endregion
#region Mounts
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new()
{
Source = hostPath,
Target = "/home/container",
ReadOnly = false,
Type = "bind"
});
#endregion
#region Port Bindings
if (true) // TODO: Add network toggle
{
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
foreach (var allocation in serverConfiguration.Allocations)
{
parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new());
parameters.ExposedPorts.Add($"{allocation.Port}/udp", new());
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/tcp", new List<PortBinding>
{
new()
{
HostPort = allocation.Port.ToString(),
HostIP = allocation.IpAddress
}
});
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/udp", new List<PortBinding>
{
new()
{
HostPort = allocation.Port.ToString(),
HostIP = allocation.IpAddress
}
});
}
}
#endregion
// TODO: Implement a way to directly startup a server without using the entrypoint.sh and parsing the startup command here
// in the daemon instead of letting it the entrypoint do. iirc pelican wants to do that as well so we need to do that
// sooner or later in order to stay compatible to pelican
// Possible flag name: LegacyEntrypointMode
return parameters;
}
public CreateContainerParameters ToInstallParameters(
ServerConfiguration serverConfiguration,
ServerInstallDataResponse installData,
string runtimeHostPath,
string installationHostPath,
string containerName
)
{
var parameters = ToSharedParameters(serverConfiguration);
// - Name
parameters.Name = containerName;
parameters.Hostname = containerName;
// - Image
parameters.Image = installData.DockerImage;
// -- Working directory
parameters.WorkingDir = "/mnt/server";
// - User
// Note: Some images might not work if we set a user here
var userId = Syscall.getuid();
// If we are root, we are able to change owner permissions after the installation
// so we run the installation as root, otherwise we need to run it as our current user,
// so we are able to access the files created by the installer
if (userId == 0)
parameters.User = "0:0";
else
parameters.User = $"{userId}:{userId}";
// -- Mounts
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new()
{
Source = runtimeHostPath,
Target = "/mnt/server",
ReadOnly = false,
Type = "bind"
});
parameters.HostConfig.Mounts.Add(new()
{
Source = installationHostPath,
Target = "/mnt/install",
ReadOnly = false,
Type = "bind"
});
parameters.Cmd = [installData.Shell, "/mnt/install/install.sh"];
return parameters;
}
public CreateContainerParameters ToSharedParameters(ServerConfiguration serverConfiguration)
{
var parameters = new CreateContainerParameters()
{
HostConfig = new()
};
#region Input, output & error streams and tty
parameters.Tty = true;
parameters.AttachStderr = true;
parameters.AttachStdin = true;
parameters.AttachStdout = true;
parameters.OpenStdin = true;
#endregion
#region CPU
parameters.HostConfig.CPUQuota = serverConfiguration.Cpu * 1000;
parameters.HostConfig.CPUPeriod = 100000;
parameters.HostConfig.CPUShares = 1024;
#endregion
#region Memory & Swap
var memoryLimit = serverConfiguration.Memory;
// The overhead multiplier gives the container a little bit more memory to prevent crashes
var memoryOverhead = memoryLimit + (memoryLimit * AppConfiguration.Server.MemoryOverheadMultiplier);
long swapLimit = -1;
/*
// If swap is enabled globally and not disabled on this server, set swap
if (!configuration.Limits.DisableSwap && config.Server.EnableSwap)
swapLimit = (long)(memoryOverhead + memoryOverhead * config.Server.SwapMultiplier);
co
*/
// Finalize limits by converting and updating the host config
parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes;
parameters.HostConfig.MemoryReservation = ByteConverter.FromMegaBytes(memoryLimit, 1000).Bytes;
parameters.HostConfig.MemorySwap =
swapLimit == -1 ? swapLimit : ByteConverter.FromMegaBytes(swapLimit, 1000).Bytes;
#endregion
#region Misc Limits
// -- Other limits
parameters.HostConfig.BlkioWeight = 100;
//container.HostConfig.PidsLimit = configuration.Limits.PidsLimit;
parameters.HostConfig.OomKillDisable = true; //!configuration.Limits.EnableOomKill;
#endregion
#region DNS
// TODO: Read hosts dns settings?
parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List<string>()
{
"1.1.1.1",
"9.9.9.9"
};
#endregion
#region Tmpfs
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
{
{ "/tmp", $"rw,exec,nosuid,size={AppConfiguration.Server.TmpFsSize}M" }
};
#endregion
#region Logging
parameters.HostConfig.LogConfig = new()
{
Type = "json-file", // We need to use this provider, as the GetLogs endpoint needs it
Config = new Dictionary<string, string>()
};
#endregion
#region Labels
parameters.Labels = new Dictionary<string, string>();
parameters.Labels.Add("Software", "Moonlight-Panel");
parameters.Labels.Add("ServerId", serverConfiguration.Id.ToString());
#endregion
#region Environment
parameters.Env = CreateEnvironmentVariables(serverConfiguration);
#endregion
return parameters;
}
private List<string> CreateEnvironmentVariables(ServerConfiguration serverConfiguration)
{
var result = new Dictionary<string, string>
{
//TODO: Add timezone, add server ip
{ "STARTUP", serverConfiguration.StartupCommand },
{ "SERVER_MEMORY", serverConfiguration.Memory.ToString() }
};
if (serverConfiguration.Allocations.Length > 0)
{
for (var i = 0; i < serverConfiguration.Allocations.Length; i++)
{
var allocation = serverConfiguration.Allocations[i];
result.Add($"ML_PORT_{i}", allocation.Port.ToString());
if (i == 0) // TODO: Implement a way to set the default/main allocation
{
result.Add("SERVER_IP", allocation.IpAddress);
result.Add("SERVER_PORT", allocation.Port.ToString());
}
}
}
// Copy variables as env vars
foreach (var variable in serverConfiguration.Variables)
result.Add(variable.Key, variable.Value);
// Convert to the format of the docker library
return result.Select(variable => $"{variable.Key}={variable.Value}").ToList();
}
}

View File

@@ -8,8 +8,6 @@ public class ServerConfiguration
public int Cpu { get; set; }
public int Memory { get; set; }
public int Disk { get; set; }
public int Bandwidth { get; set; }
public bool UseVirtualDisk { get; set; }
// Start, Stop & Status
public string StartupCommand { get; set; }

View File

@@ -14,7 +14,7 @@ public class ServerConsole
MaxMessagesInCache = maxMessagesInCache;
}
public async Task WriteToOutput(string content)
public async Task WriteToOutputAsync(string content)
{
lock (MessageCache)
{
@@ -32,7 +32,7 @@ public class ServerConsole
}
}
public async Task WriteToInput(string content)
public async Task WriteToInputAsync(string content)
{
if (OnInput != null)
await OnInput.Invoke(content);

View File

@@ -8,13 +8,13 @@
<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="MoonCore" Version="1.8.6" />
<PackageReference Include="MoonCore.Extended" Version="1.3.3" />
<PackageReference Include="MoonCore.Unix" Version="1.0.7" />
<PackageReference Include="MoonCore" Version="2.0.1" />
<PackageReference Include="MoonCore.Extended" Version="1.4.0" />
<PackageReference Include="MoonCore.Unix" Version="1.0.8" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Stateless" Version="5.17.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1"/>
<PackageReference Include="Stateless" Version="5.19.0" />
</ItemGroup>
<ItemGroup>
@@ -49,6 +49,21 @@
<_ContentIncludedByDefault Remove="storage\volumes\2\usercache.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\version_history.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\whitelist.json" />
<_ContentIncludedByDefault Remove="volumes\3\banned-ips.json" />
<_ContentIncludedByDefault Remove="volumes\3\banned-players.json" />
<_ContentIncludedByDefault Remove="volumes\3\ops.json" />
<_ContentIncludedByDefault Remove="volumes\3\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="volumes\3\usercache.json" />
<_ContentIncludedByDefault Remove="volumes\3\version_history.json" />
<_ContentIncludedByDefault Remove="volumes\3\whitelist.json" />
<_ContentIncludedByDefault Remove="storage\volumes\69\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\banned-ips.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\banned-players.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\ops.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\usercache.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\version_history.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\whitelist.json" />
</ItemGroup>
</Project>

View File

@@ -2,4 +2,4 @@ using MoonlightServers.Daemon;
var startup = new Startup();
await startup.Run(args);
await startup.RunAsync(args);

View File

@@ -0,0 +1,221 @@
using System.Text;
using Docker.DotNet;
using MoonCore.Events;
using MoonCore.Helpers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerConsole : IConsole
{
private readonly EventSource<string> StdOutEventSource = new();
private readonly ConcurrentList<string> StdOutCache = new();
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
private readonly ILogger Logger;
private MultiplexedStream? CurrentStream;
private CancellationTokenSource Cts = new();
public DockerConsole(DockerClient dockerClient, ServerContext context)
{
DockerClient = dockerClient;
Context = context;
Logger = Context.Logger;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task WriteStdInAsync(string content)
{
if (CurrentStream == null)
{
Logger.LogWarning("Unable to write to stdin as no stream is connected");
return;
}
var contextBuffer = Encoding.UTF8.GetBytes(content);
await CurrentStream.WriteAsync(contextBuffer, 0, contextBuffer.Length, Cts.Token);
}
public async Task WriteStdOutAsync(string content)
{
// Add output cache
if (StdOutCache.Count > 250) // TODO: Config
StdOutCache.RemoveRange(0, 100);
StdOutCache.Add(content);
// Fire event
await StdOutEventSource.InvokeAsync(content);
}
public async Task AttachRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await AttachToContainerAsync(containerName);
}
public async Task AttachInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
await AttachToContainerAsync(containerName);
}
private async Task AttachToContainerAsync(string containerName)
{
var cts = new CancellationTokenSource();
// Cancels previous active read task if it exists
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
// Update the current cancellation token
Cts = cts;
// Start reading task
Task.Run(async () =>
{
// This loop is here to reconnect to the stream when connection is lost.
// This can occur when docker restarts for example
while (!cts.IsCancellationRequested)
{
MultiplexedStream? innerStream = null;
try
{
Logger.LogTrace("Attaching");
innerStream = await DockerClient.Containers.AttachContainerAsync(
containerName,
true,
new()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
cts.Token
);
CurrentStream = innerStream;
var buffer = new byte[1024];
try
{
// Read while server tasks are not canceled
while (!cts.Token.IsCancellationRequested)
{
var readResult = await innerStream.ReadOutputAsync(
buffer,
0,
buffer.Length,
cts.Token
);
if (readResult.EOF)
await cts.CancelAsync();
var decodedText = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
await WriteStdOutAsync(decodedText);
}
Logger.LogTrace("Read loop exited");
}
catch (TaskCanceledException)
{
// Ignored
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogWarning(e, "An unhandled error occured while reading from container stream");
}
}
catch (TaskCanceledException)
{
// ignored
}
catch (DockerContainerNotFoundException)
{
// Container got removed. Stop the reconnect loop
Logger.LogDebug("Container '{name}' got removed. Stopping reconnect stream for console", containerName);
await cts.CancelAsync();
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while attaching to container");
}
innerStream?.Dispose();
}
Logger.LogDebug("Disconnected from container stream");
});
}
public async Task FetchRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await FetchFromContainerAsync(containerName);
}
public async Task FetchInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
await FetchFromContainerAsync(containerName);
}
private async Task FetchFromContainerAsync(string containerName)
{
var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
});
var combinedOutput = await logStream.ReadOutputToEndAsync(Cts.Token);
var contentToAdd = combinedOutput.stdout + combinedOutput.stderr;
await WriteStdOutAsync(contentToAdd);
}
public Task ClearCacheAsync()
{
StdOutCache.Clear();
return Task.CompletedTask;
}
public Task<IEnumerable<string>> GetCacheAsync()
{
return Task.FromResult<IEnumerable<string>>(StdOutCache);
}
public async Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback)
=> await StdOutEventSource.SubscribeAsync(callback);
public async ValueTask DisposeAsync()
{
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
if (CurrentStream != null)
CurrentStream.Dispose();
}
}

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public static class DockerConstants
{
public const string RuntimeNameTemplate = "moonlight-runtime-{0}";
public const string InstallationNameTemplate = "moonlight-installation-{0}";
}

View File

@@ -0,0 +1,184 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Events;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerInstallation : IInstallation
{
private readonly DockerEventService DockerEventService;
private readonly ServerConfigurationMapper Mapper;
private readonly DockerImageService ImageService;
private readonly ServerContext ServerContext;
private readonly DockerClient DockerClient;
private IReporter Reporter => ServerContext.Server.Reporter;
private readonly EventSource<int> ExitEventSource = new();
private IAsyncDisposable ContainerEventSubscription;
private string ContainerId;
public DockerInstallation(
DockerClient dockerClient,
ServerContext serverContext,
ServerConfigurationMapper mapper,
DockerImageService imageService,
DockerEventService dockerEventService
)
{
DockerClient = dockerClient;
ServerContext = serverContext;
Mapper = mapper;
ImageService = imageService;
DockerEventService = dockerEventService;
}
public async Task InitializeAsync()
{
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
}
private async ValueTask OnContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return;
// Only handle die events
if (message.Action != "die")
return;
int exitCode;
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
{
if (!int.TryParse(exitCodeStr, out exitCode))
exitCode = 0;
}
else
exitCode = 0;
await ExitEventSource.InvokeAsync(exitCode);
}
public async Task<bool> CheckExistsAsync()
{
try
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.InspectContainerAsync(
containerName
);
return true;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task CreateAsync(
string runtimePath,
string hostPath,
ServerInstallDataResponse data
)
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
var parameters = Mapper.ToInstallParameters(
ServerContext.Configuration,
data,
runtimePath,
hostPath,
containerName
);
// Docker image
await Reporter.StatusAsync("Downloading docker image");
await ImageService.DownloadAsync(data.DockerImage, async status => { await Reporter.StatusAsync(status); });
await Reporter.StatusAsync("Downloaded docker image");
// Write install script to install fs
await File.WriteAllTextAsync(
Path.Combine(hostPath, "install.sh"),
data.Script
);
//
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = response.ID;
await Reporter.StatusAsync("Created container");
}
public async Task StartAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
public async Task KillAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.KillContainerAsync(containerName, new());
}
public async Task DestroyAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
if (container.State.Running)
await DockerClient.Containers.KillContainerAsync(containerName, new());
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
{
Force = true
});
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
public async Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback)
=> await ExitEventSource.SubscribeAsync(callback);
public async Task RestoreAsync()
{
try
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
ContainerId = container.ID;
}
catch (DockerContainerNotFoundException)
{
// Ignore
}
}
public async ValueTask DisposeAsync()
{
await ContainerEventSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,59 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerRestorer : IRestorer
{
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
public DockerRestorer(DockerClient dockerClient, ServerContext context)
{
DockerClient = dockerClient;
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task<bool> HandleRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(
containerName
);
return container.State.Running;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task<bool> HandleInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(
containerName
);
return container.State.Running;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,177 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Events;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerRuntime : IRuntime
{
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
private readonly ServerConfigurationMapper Mapper;
private readonly DockerEventService DockerEventService;
private readonly DockerImageService ImageService;
private readonly EventSource<int> ExitEventSource = new();
private IReporter Reporter => Context.Server.Reporter;
private IAsyncDisposable ContainerEventSubscription;
private string ContainerId;
public DockerRuntime(
DockerClient dockerClient,
ServerContext context,
ServerConfigurationMapper mapper,
DockerEventService dockerEventService,
DockerImageService imageService
)
{
DockerClient = dockerClient;
Context = context;
Mapper = mapper;
DockerEventService = dockerEventService;
ImageService = imageService;
}
public async Task InitializeAsync()
{
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
}
private async ValueTask OnContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return;
// Only handle die events
if (message.Action != "die")
return;
int exitCode;
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
{
if (!int.TryParse(exitCodeStr, out exitCode))
exitCode = 0;
}
else
exitCode = 0;
await ExitEventSource.InvokeAsync(exitCode);
}
public async Task<bool> CheckExistsAsync()
{
try
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.InspectContainerAsync(
containerName
);
return true;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task CreateAsync(string path)
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
var parameters = Mapper.ToRuntimeParameters(
Context.Configuration,
path,
containerName
);
// Docker image
await Reporter.StatusAsync("Downloading docker image");
await ImageService.DownloadAsync(
Context.Configuration.DockerImage,
async status => { await Reporter.StatusAsync(status); }
);
await Reporter.StatusAsync("Downloaded docker image");
//
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = response.ID;
await Reporter.StatusAsync("Created container");
}
public async Task StartAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
public Task UpdateAsync()
{
return Task.CompletedTask;
}
public async Task KillAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.KillContainerAsync(containerName, new());
}
public async Task DestroyAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
if (container.State.Running)
await DockerClient.Containers.KillContainerAsync(containerName, new());
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
{
Force = true
});
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
public async Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback)
=> await ExitEventSource.SubscribeAsync(callback);
public async Task RestoreAsync()
{
try
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
ContainerId = container.ID;
}
catch (DockerContainerNotFoundException)
{
// Ignore
}
}
public async ValueTask DisposeAsync()
{
await ContainerEventSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,25 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerStatistics : IStatistics
{
public Task InitializeAsync()
=> Task.CompletedTask;
public Task AttachRuntimeAsync()
=> Task.CompletedTask;
public Task AttachInstallationAsync()
=> Task.CompletedTask;
public Task ClearCacheAsync()
=> Task.CompletedTask;
public Task<IEnumerable<StatisticsData>> GetCacheAsync()
=> Task.FromResult<IEnumerable<StatisticsData>>([]);
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,4 +1,4 @@
namespace MoonlightServers.Daemon.ServerSystem;
namespace MoonlightServers.Daemon.ServerSystem.Enums;
public enum ServerState
{
@@ -6,5 +6,6 @@ public enum ServerState
Starting = 1,
Online = 2,
Stopping = 3,
Installing = 4
Installing = 4,
Locked = 5
}

View File

@@ -0,0 +1,12 @@
namespace MoonlightServers.Daemon.ServerSystem.Enums;
public enum ServerTrigger
{
Start = 0,
Stop = 1,
Kill = 2,
DetectOnline = 3,
Install = 4,
Fail = 5,
Exited = 6
}

View File

@@ -0,0 +1,58 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.FileSystems;
public class RawInstallationFs : IFileSystem
{
private readonly string BaseDirectory;
public RawInstallationFs(ServerContext context)
{
BaseDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"storage",
"install",
context.Configuration.Id.ToString()
);
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task<string> GetPathAsync()
=> Task.FromResult(BaseDirectory);
public Task<bool> CheckExistsAsync()
{
var exists = Directory.Exists(BaseDirectory);
return Task.FromResult(exists);
}
public Task<bool> CheckMountedAsync()
=> Task.FromResult(true);
public Task CreateAsync()
{
Directory.CreateDirectory(BaseDirectory);
return Task.CompletedTask;
}
public Task PerformChecksAsync()
=> Task.CompletedTask;
public Task MountAsync()
=> Task.CompletedTask;
public Task UnmountAsync()
=> Task.CompletedTask;
public Task DestroyAsync()
{
Directory.Delete(BaseDirectory, true);
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,58 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.FileSystems;
public class RawRuntimeFs : IFileSystem
{
private readonly string BaseDirectory;
public RawRuntimeFs(ServerContext context)
{
BaseDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"storage",
"volumes",
context.Configuration.Id.ToString()
);
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task<string> GetPathAsync()
=> Task.FromResult(BaseDirectory);
public Task<bool> CheckExistsAsync()
{
var exists = Directory.Exists(BaseDirectory);
return Task.FromResult(exists);
}
public Task<bool> CheckMountedAsync()
=> Task.FromResult(true);
public Task CreateAsync()
{
Directory.CreateDirectory(BaseDirectory);
return Task.CompletedTask;
}
public Task PerformChecksAsync()
=> Task.CompletedTask;
public Task MountAsync()
=> Task.CompletedTask;
public Task UnmountAsync()
=> Task.CompletedTask;
public Task DestroyAsync()
{
Directory.Delete(BaseDirectory, true);
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,35 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class DebugHandler : IServerStateHandler
{
private readonly ServerContext Context;
private IAsyncDisposable? StdOutSubscription;
public DebugHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if(StdOutSubscription != null)
return;
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(line =>
{
Console.WriteLine($"STD OUT: {line}");
return ValueTask.CompletedTask;
});
}
public async ValueTask DisposeAsync()
{
if (StdOutSubscription != null)
await StdOutSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,125 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class InstallationHandler : IServerStateHandler
{
private readonly ServerContext Context;
private Server Server => Context.Server;
private IAsyncDisposable? ExitSubscription;
public InstallationHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if (transition is
{ Source: ServerState.Offline, Destination: ServerState.Installing, Trigger: ServerTrigger.Install })
{
await StartAsync();
}
else if (transition is
{ Source: ServerState.Installing, Destination: ServerState.Offline, Trigger: ServerTrigger.Exited })
{
await CompleteAsync();
}
}
private async Task StartAsync()
{
// Plan:
// 1. Fetch latest configuration
// 2. Check if both file systems exists
// 3. Check if both file systems are mounted
// 4. Run file system checks
// 5. Create installation container
// 6. Attach console
// 7. Start installation container
// 1. Fetch latest configuration
var installData = new ServerInstallDataResponse()
{
Script = await File.ReadAllTextAsync(Path.Combine("storage", "install.sh")),
Shell = "/bin/ash",
DockerImage = "ghcr.io/parkervcp/installers:alpine"
};
// 2. Check if file system exists
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
await Server.RuntimeFileSystem.CreateAsync();
if (!await Server.InstallationFileSystem.CheckExistsAsync())
await Server.InstallationFileSystem.CreateAsync();
// 3. Check if both file systems are mounted
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
await Server.RuntimeFileSystem.MountAsync();
if (!await Server.InstallationFileSystem.CheckMountedAsync())
await Server.InstallationFileSystem.MountAsync();
// 4. Run file system checks
await Server.RuntimeFileSystem.PerformChecksAsync();
await Server.InstallationFileSystem.PerformChecksAsync();
// 5. Create installation
var runtimePath = await Server.RuntimeFileSystem.GetPathAsync();
var installationPath = await Server.InstallationFileSystem.GetPathAsync();
if (await Server.Installation.CheckExistsAsync())
await Server.Installation.DestroyAsync();
await Server.Installation.CreateAsync(runtimePath, installationPath, installData);
if (ExitSubscription == null)
ExitSubscription = await Server.Installation.SubscribeExitedAsync(OnInstallationExited);
// 6. Attach console
await Server.Console.AttachInstallationAsync();
// 7. Start installation container
await Server.Installation.StartAsync();
}
private async ValueTask OnInstallationExited(int exitCode)
{
// TODO: Notify the crash handler component of the exit code
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
}
private async Task CompleteAsync()
{
// Plan:
// 1. Handle possible crash
// 2. Remove installation container
// 3. Remove installation file system
// 1. Handle possible crash
// TODO
// 2. Remove installation container
await Server.Installation.DestroyAsync();
// 3. Remove installation file system
await Server.InstallationFileSystem.UnmountAsync();
await Server.InstallationFileSystem.DestroyAsync();
Context.Logger.LogDebug("Completed installation");
}
public async ValueTask DisposeAsync()
{
if (ExitSubscription != null)
await ExitSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,85 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class OnlineDetectionHandler : IServerStateHandler
{
private readonly ServerContext Context;
private IOnlineDetector OnlineDetector => Context.Server.OnlineDetector;
private ILogger Logger => Context.Logger;
private IAsyncDisposable? ConsoleSubscription;
private bool IsActive = false;
public OnlineDetectionHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if (
transition is
{ Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start } && !IsActive
)
{
await StartAsync();
}
else if (transition is { Source: not ServerState.Installing, Destination: ServerState.Offline } && IsActive)
{
await StopAsync();
}
}
private async Task StartAsync()
{
IsActive = true;
await OnlineDetector.CreateAsync();
ConsoleSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnHandleOutput);
Logger.LogTrace("Created online detector. Created console subscription");
}
private async ValueTask OnHandleOutput(string line)
{
if(!IsActive)
return;
if(!await OnlineDetector.HandleOutputAsync(line))
return;
if(!Context.Server.StateMachine.CanFire(ServerTrigger.DetectOnline))
return;
Logger.LogTrace("Detected server as online. Destroying online detector");
await Context.Server.StateMachine.FireAsync(ServerTrigger.DetectOnline);
await StopAsync();
}
private async Task StopAsync()
{
IsActive = false;
if (ConsoleSubscription != null)
{
await ConsoleSubscription.DisposeAsync();
ConsoleSubscription = null;
}
await OnlineDetector.DestroyAsync();
Logger.LogTrace("Destroyed online detector. Revoked console subscription");
}
public async ValueTask DisposeAsync()
{
if (ConsoleSubscription != null)
await ConsoleSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,42 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class ShutdownHandler : IServerStateHandler
{
private readonly ServerContext ServerContext;
public ShutdownHandler(ServerContext serverContext)
{
ServerContext = serverContext;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
// Filter (we only want to handle exists from the runtime, so we filter out the installing state)
if (transition is not
{
Destination: ServerState.Offline,
Source: not ServerState.Installing,
Trigger: ServerTrigger.Exited // We don't want to handle the fail event here
})
return;
// Plan:
// 1. Handle possible crash
// 2. Remove runtime
// 1. Handle possible crash
// TODO: Handle crash here
// 2. Remove runtime
await ServerContext.Server.Runtime.DestroyAsync();
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,84 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class StartupHandler : IServerStateHandler
{
private IAsyncDisposable? ExitSubscription;
private readonly ServerContext Context;
private Server Server => Context.Server;
public StartupHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
// Filter
if (transition is not {Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start})
return;
// Plan:
// 1. Fetch latest configuration
// 2. Check if file system exists
// 3. Check if file system is mounted
// 4. Run file system checks
// 5. Create runtime
// 6. Attach console
// 7. Start runtime
// 1. Fetch latest configuration
// TODO
// Consider moving it out of the startup handler, as other handlers might need
// the updated config as well or add sorting into the handler registration to ensure they are executing in the correct order.
// Sort when building server, not when executing handlers
// 2. Check if file system exists
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
await Server.RuntimeFileSystem.CreateAsync();
// 3. Check if file system is mounted
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
await Server.RuntimeFileSystem.CheckMountedAsync();
// 4. Run file system checks
await Server.RuntimeFileSystem.PerformChecksAsync();
// 5. Create runtime
var hostPath = await Server.RuntimeFileSystem.GetPathAsync();
if (await Server.Runtime.CheckExistsAsync())
await Server.Runtime.DestroyAsync();
await Server.Runtime.CreateAsync(hostPath);
if (ExitSubscription == null)
ExitSubscription = await Server.Runtime.SubscribeExitedAsync(OnRuntimeExited);
// 6. Attach console
await Server.Console.AttachRuntimeAsync();
// 7. Start runtime
await Server.Runtime.StartAsync();
}
private async ValueTask OnRuntimeExited(int exitCode)
{
// TODO: Notify the crash handler component of the exit code
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
}
public async ValueTask DisposeAsync()
{
if (ExitSubscription != null)
await ExitSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class ConsoleSignalRComponent : IServerComponent
{
private readonly IHubContext<ServerWebSocketHub> Hub;
private readonly ServerContext Context;
private IAsyncDisposable? StdOutSubscription;
private string HubGroup;
public ConsoleSignalRComponent(IHubContext<ServerWebSocketHub> hub, ServerContext context)
{
Hub = hub;
Context = context;
}
public async Task InitializeAsync()
{
HubGroup = Context.Configuration.Id.ToString();
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnStdOut);
}
private async ValueTask OnStdOut(string output)
{
await Hub.Clients.Group(HubGroup).SendAsync("ConsoleOutput", output);
}
public async ValueTask DisposeAsync()
{
if (StdOutSubscription != null)
await StdOutSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,52 @@
using System.Text.RegularExpressions;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class RegexOnlineDetector : IOnlineDetector
{
private readonly ServerContext Context;
private Regex? Expression;
public RegexOnlineDetector(ServerContext context)
{
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task CreateAsync()
{
if(string.IsNullOrEmpty(Context.Configuration.OnlineDetection))
return Task.CompletedTask;
Expression = new Regex(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
return Task.CompletedTask;
}
public Task<bool> HandleOutputAsync(string line)
{
if (Expression == null)
return Task.FromResult(false);
var result = Expression.Matches(line).Count > 0;
return Task.FromResult(result);
}
public Task DestroyAsync()
{
Expression = null;
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
{
Expression = null;
return ValueTask.CompletedTask;
}
}

View File

@@ -0,0 +1,44 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class ServerReporter : IReporter
{
private readonly ServerContext Context;
private const string StatusTemplate =
"\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[3;38;2;200;200;200m{0}\x1b[0m\n\r";
private const string ErrorTemplate =
"\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[1;38;2;255;0;0m{0}\x1b[0m\n\r";
public ServerReporter(ServerContext context)
{
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task StatusAsync(string message)
{
Context.Logger.LogInformation("Status: {message}", message);
await Context.Server.Console.WriteStdOutAsync(
string.Format(StatusTemplate, message)
);
}
public async Task ErrorAsync(string message)
{
Context.Logger.LogError("Error: {message}", message);
await Context.Server.Console.WriteStdOutAsync(
string.Format(ErrorTemplate, message)
);
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,64 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IConsole : IServerComponent
{
/// <summary>
/// Writes to the standard input of the console. If attached to the runtime when using docker for example this
/// would write into the containers standard input.
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
/// </summary>
/// <param name="content">Content to write</param>
/// <returns></returns>
public Task WriteStdInAsync(string content);
/// <summary>
/// Writes to the standard output of the console. If attached to the runtime when using docker for example this
/// would write into the containers standard output.
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
/// </summary>
/// <param name="content">Content to write</param>
/// <returns></returns>
public Task WriteStdOutAsync(string content);
/// <summary>
/// Attaches the console to the runtime environment
/// </summary>
/// <returns></returns>
public Task AttachRuntimeAsync();
/// <summary>
/// Attaches the console to the installation environment
/// </summary>
/// <returns></returns>
public Task AttachInstallationAsync();
/// <summary>
/// Fetches all output from the runtime environment and write them into the cache without triggering any events
/// </summary>
/// <returns></returns>
public Task FetchRuntimeAsync();
/// <summary>
/// Fetches all output from the installation environment and write them into the cache without triggering any events
/// </summary>
/// <returns></returns>
public Task FetchInstallationAsync();
/// <summary>
/// Clears the cache of the standard output received by the environments
/// </summary>
/// <returns></returns>
public Task ClearCacheAsync();
/// <summary>
/// Gets the content from the standard output cache
/// </summary>
/// <returns>Content from the cache</returns>
public Task<IEnumerable<string>> GetCacheAsync();
/// <summary>
/// Subscribes to standard output receive events
/// </summary>
/// <param name="callback">Callback which will be invoked whenever a new line is received</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback);
}

View File

@@ -0,0 +1,54 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IFileSystem : IServerComponent
{
/// <summary>
/// Gets the path of the file system on the host operating system to be reused by other components
/// </summary>
/// <returns>Path to the file systems storage location</returns>
public Task<string> GetPathAsync();
/// <summary>
/// Checks if the file system exists
/// </summary>
/// <returns>True if it does exist. False if it doesn't exist</returns>
public Task<bool> CheckExistsAsync();
/// <summary>
/// Checks if the file system is mounted
/// </summary>
/// <returns>True if its mounted, False if it is not mounted</returns>
public Task<bool> CheckMountedAsync();
/// <summary>
/// Creates the file system. E.g. Creating a virtual disk, formatting it
/// </summary>
/// <returns></returns>
public Task CreateAsync();
/// <summary>
/// Performs checks and optimisations on the file system.
/// E.g. checking for corrupted files, resizing a virtual disk or adjusting file permissions
/// <remarks>Requires <see cref="MountAsync"/> to be called before or the file system to be in a mounted state</remarks>
/// </summary>
/// <returns></returns>
public Task PerformChecksAsync();
/// <summary>
/// Mounts the file system
/// </summary>
/// <returns></returns>
public Task MountAsync();
/// <summary>
/// Unmounts the file system
/// </summary>
/// <returns></returns>
public Task UnmountAsync();
/// <summary>
/// Destroys the file system and its contents
/// </summary>
/// <returns></returns>
public Task DestroyAsync();
}

View File

@@ -0,0 +1,53 @@
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IInstallation : IServerComponent
{
/// <summary>
/// Checks if the installation environment exists. It doesn't matter if it is currently running or not
/// </summary>
/// <returns>True if it exists, False if it doesn't</returns>
public Task<bool> CheckExistsAsync();
/// <summary>
/// Creates the installation environment
/// </summary>
/// <param name="runtimePath">Host path of the runtime storage location</param>
/// <param name="hostPath">Host path of the installation file system</param>
/// <param name="data">Installation data for the server</param>
/// <returns></returns>
public Task CreateAsync(string runtimePath, string hostPath, ServerInstallDataResponse data);
/// <summary>
/// Starts the installation
/// </summary>
/// <returns></returns>
public Task StartAsync();
/// <summary>
/// Kills the current installation immediately
/// </summary>
/// <returns></returns>
public Task KillAsync();
/// <summary>
/// Removes the installation. E.g. removes the docker container
/// </summary>
/// <returns></returns>
public Task DestroyAsync();
/// <summary>
/// Subscribes to the event when the installation exists
/// </summary>
/// <param name="callback">Callback to invoke whenever the installation exists</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback);
/// <summary>
/// Connects an existing installation to this abstraction in order to restore it.
/// E.g. fetching the container id and using it for exit events
/// </summary>
/// <returns></returns>
public Task RestoreAsync();
}

View File

@@ -0,0 +1,23 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IOnlineDetector : IServerComponent
{
/// <summary>
/// Creates the detection engine for the online state
/// </summary>
/// <returns></returns>
public Task CreateAsync();
/// <summary>
/// Handles the detection of the online state based on the received output
/// </summary>
/// <param name="line">Excerpt of the output</param>
/// <returns>True if the detection showed that the server is online. False if the detection didnt find anything</returns>
public Task<bool> HandleOutputAsync(string line);
/// <summary>
/// Destroys the detection engine for the online state
/// </summary>
/// <returns></returns>
public Task DestroyAsync();
}

View File

@@ -0,0 +1,18 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IReporter : IServerComponent
{
/// <summary>
/// Writes both in the server logs as well in the server console the provided message as a status update
/// </summary>
/// <param name="message">Message to write</param>
/// <returns></returns>
public Task StatusAsync(string message);
/// <summary>
/// Writes both in the server logs as well in the server console the provided message as an error
/// </summary>
/// <param name="message">Message to write</param>
/// <returns></returns>
public Task ErrorAsync(string message);
}

View File

@@ -0,0 +1,16 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IRestorer : IServerComponent
{
/// <summary>
/// Checks for any running runtime environment from which the state can be restored from
/// </summary>
/// <returns></returns>
public Task<bool> HandleRuntimeAsync();
/// <summary>
/// Checks for any running installation environment from which the state can be restored from
/// </summary>
/// <returns></returns>
public Task<bool> HandleInstallationAsync();
}

Some files were not shown because too many files have changed in this diff Show More