Started implementing server share backend. Redesigned server authorization for api calls. Refactored controller names for servers. Moved some responses to correct namespace

This commit is contained in:
2025-06-05 23:35:39 +02:00
parent 4b1045d629
commit 1ec4450040
37 changed files with 1169 additions and 139 deletions

View File

@@ -10,6 +10,7 @@ public class Server
public List<Allocation> Allocations { get; set; } = new(); public List<Allocation> Allocations { get; set; } = new();
public List<ServerVariable> Variables { get; set; } = new(); public List<ServerVariable> Variables { get; set; } = new();
public List<ServerBackup> Backups { get; set; } = new(); public List<ServerBackup> Backups { get; set; } = new();
public List<ServerShare> Shares { get; set; } = new();
// Meta // Meta
public string Name { get; set; } public string Name { get; set; }

View File

@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace MoonlightServers.ApiServer.Database.Entities;
public class ServerShare
{
public int Id { get; set; }
public int UserId { get; set; }
public Server Server { get; set; }
[Column(TypeName = "jsonb")]
public string Permissions { get; set; }
[Column(TypeName="timestamp with time zone")]
public DateTime CreatedAt { get; set; }
[Column(TypeName="timestamp with time zone")]
public DateTime UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,500 @@
// <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("20250605210823_AddedServerShares")]
partial class AddedServerShares
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.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.ServerShare", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Permissions")
.IsRequired()
.HasColumnType("jsonb");
b.Property<int>("ServerId")
.HasColumnType("integer");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("Servers_ServerShares", (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.ServerShare", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Shares")
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Server");
});
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("Shares");
b.Navigation("Variables");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedServerShares : 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),
Permissions = table.Column<string>(type: "jsonb", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", 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

@@ -17,7 +17,7 @@ namespace MoonlightServers.ApiServer.Database.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -180,6 +180,37 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.ToTable("Servers_ServerBackups", (string)null); b.ToTable("Servers_ServerBackups", (string)null);
}); });
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerShare", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Permissions")
.IsRequired()
.HasColumnType("jsonb");
b.Property<int>("ServerId")
.HasColumnType("integer");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("Servers_ServerShares", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b => modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -392,6 +423,17 @@ namespace MoonlightServers.ApiServer.Database.Migrations
.HasForeignKey("ServerId"); .HasForeignKey("ServerId");
}); });
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerShare", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Shares")
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b => modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{ {
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server") b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
@@ -438,6 +480,8 @@ namespace MoonlightServers.ApiServer.Database.Migrations
b.Navigation("Backups"); b.Navigation("Backups");
b.Navigation("Shares");
b.Navigation("Variables"); b.Navigation("Variables");
}); });

View File

@@ -13,6 +13,7 @@ public class ServersDataContext : DatabaseContext
public DbSet<Node> Nodes { get; set; } public DbSet<Node> Nodes { get; set; }
public DbSet<Server> Servers { get; set; } public DbSet<Server> Servers { get; set; }
public DbSet<ServerBackup> ServerBackups { get; set; } public DbSet<ServerBackup> ServerBackups { get; set; }
public DbSet<ServerShare> ServerShares { get; set; }
public DbSet<ServerVariable> ServerVariables { get; set; } public DbSet<ServerVariable> ServerVariables { get; set; }
public DbSet<Star> Stars { get; set; } public DbSet<Star> Stars { get; set; }
public DbSet<StarDockerImage> StarDockerImages { get; set; } public DbSet<StarDockerImage> StarDockerImages { get; set; }

View File

@@ -7,6 +7,7 @@ using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services; using MoonlightServers.ApiServer.Services;
using MoonlightServers.DaemonShared.Enums; using MoonlightServers.DaemonShared.Enums;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Files; using MoonlightServers.Shared.Http.Requests.Client.Servers.Files;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Files; using MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
@@ -15,33 +16,30 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize] [Authorize]
[ApiController] [ApiController]
[Route("api/client/servers")] [Route("api/client/servers")]
public class ServerFileSystemController : Controller public class FilesController : Controller
{ {
private readonly DatabaseRepository<Server> ServerRepository; private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly ServerFileSystemService ServerFileSystemService; private readonly ServerFileSystemService ServerFileSystemService;
private readonly ServerService ServerService;
private readonly NodeService NodeService; private readonly NodeService NodeService;
private readonly ServerAuthorizeService AuthorizeService;
public ServerFileSystemController( public FilesController(
DatabaseRepository<Server> serverRepository, DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository,
ServerFileSystemService serverFileSystemService, ServerFileSystemService serverFileSystemService,
ServerService serverService, NodeService nodeService,
NodeService nodeService ServerAuthorizeService authorizeService
) )
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
UserRepository = userRepository;
ServerFileSystemService = serverFileSystemService; ServerFileSystemService = serverFileSystemService;
ServerService = serverService;
NodeService = nodeService; NodeService = nodeService;
AuthorizeService = authorizeService;
} }
[HttpGet("{serverId:int}/files/list")] [HttpGet("{serverId:int}/files/list")]
public async Task<ServerFilesEntryResponse[]> List([FromRoute] int serverId, [FromQuery] string path) public async Task<ServerFilesEntryResponse[]> List([FromRoute] int serverId, [FromQuery] string path)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.Read);
var entries = await ServerFileSystemService.List(server, path); var entries = await ServerFileSystemService.List(server, path);
@@ -58,7 +56,7 @@ public class ServerFileSystemController : Controller
[HttpPost("{serverId:int}/files/move")] [HttpPost("{serverId:int}/files/move")]
public async Task Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath) public async Task Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
await ServerFileSystemService.Move(server, oldPath, newPath); await ServerFileSystemService.Move(server, oldPath, newPath);
} }
@@ -66,7 +64,7 @@ public class ServerFileSystemController : Controller
[HttpDelete("{serverId:int}/files/delete")] [HttpDelete("{serverId:int}/files/delete")]
public async Task Delete([FromRoute] int serverId, [FromQuery] string path) public async Task Delete([FromRoute] int serverId, [FromQuery] string path)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
await ServerFileSystemService.Delete(server, path); await ServerFileSystemService.Delete(server, path);
} }
@@ -74,7 +72,7 @@ public class ServerFileSystemController : Controller
[HttpPost("{serverId:int}/files/mkdir")] [HttpPost("{serverId:int}/files/mkdir")]
public async Task Mkdir([FromRoute] int serverId, [FromQuery] string path) public async Task Mkdir([FromRoute] int serverId, [FromQuery] string path)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
await ServerFileSystemService.Mkdir(server, path); await ServerFileSystemService.Mkdir(server, path);
} }
@@ -82,7 +80,7 @@ public class ServerFileSystemController : Controller
[HttpGet("{serverId:int}/files/upload")] [HttpGet("{serverId:int}/files/upload")]
public async Task<ServerFilesUploadResponse> Upload([FromRoute] int serverId) public async Task<ServerFilesUploadResponse> Upload([FromRoute] int serverId)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var accessToken = NodeService.CreateAccessToken( var accessToken = NodeService.CreateAccessToken(
server.Node, server.Node,
@@ -109,7 +107,7 @@ public class ServerFileSystemController : Controller
[HttpGet("{serverId:int}/files/download")] [HttpGet("{serverId:int}/files/download")]
public async Task<ServerFilesDownloadResponse> Download([FromRoute] int serverId, [FromQuery] string path) public async Task<ServerFilesDownloadResponse> Download([FromRoute] int serverId, [FromQuery] string path)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.Read);
var accessToken = NodeService.CreateAccessToken( var accessToken = NodeService.CreateAccessToken(
server.Node, server.Node,
@@ -137,7 +135,7 @@ public class ServerFileSystemController : Controller
[HttpPost("{serverId:int}/files/compress")] [HttpPost("{serverId:int}/files/compress")]
public async Task Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request) public async Task Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
if (!Enum.TryParse(request.Type, true, out CompressType type)) if (!Enum.TryParse(request.Type, true, out CompressType type))
throw new HttpApiException("Invalid compress type provided", 400); throw new HttpApiException("Invalid compress type provided", 400);
@@ -148,7 +146,7 @@ public class ServerFileSystemController : Controller
[HttpPost("{serverId:int}/files/decompress")] [HttpPost("{serverId:int}/files/decompress")]
public async Task Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request) public async Task Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
if (!Enum.TryParse(request.Type, true, out CompressType type)) if (!Enum.TryParse(request.Type, true, out CompressType type))
throw new HttpApiException("Invalid compress type provided", 400); throw new HttpApiException("Invalid compress type provided", 400);
@@ -156,7 +154,7 @@ public class ServerFileSystemController : Controller
await ServerFileSystemService.Decompress(server, type, request.Path, request.Destination); await ServerFileSystemService.Decompress(server, type, request.Path, request.Destination);
} }
private async Task<Server> GetServerById(int serverId) private async Task<Server> GetServerById(int serverId, ServerPermissionType type)
{ {
var server = await ServerRepository var server = await ServerRepository
.Get() .Get()
@@ -166,11 +164,7 @@ public class ServerFileSystemController : Controller
if (server == null) if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
var userIdClaim = User.Claims.First(x => x.Type == "userId"); if (!await AuthorizeService.Authorize(User, server, permission => permission.Name == "files" && permission.Type >= type))
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if (!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
return server; return server;

View File

@@ -7,27 +7,31 @@ using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services; using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Http.Controllers.Client; namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[ApiController] [ApiController]
[Authorize] [Authorize]
[Route("api/client/servers")] [Route("api/client/servers")]
public class ServerPowerController : Controller public class PowerController : Controller
{ {
private readonly DatabaseRepository<Server> ServerRepository; private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository; private readonly DatabaseRepository<User> UserRepository;
private readonly ServerService ServerService; private readonly ServerService ServerService;
private readonly ServerAuthorizeService AuthorizeService;
public ServerPowerController( public PowerController(
DatabaseRepository<Server> serverRepository, DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository, DatabaseRepository<User> userRepository,
ServerService serverService ServerService serverService,
ServerAuthorizeService authorizeService
) )
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
UserRepository = userRepository; UserRepository = userRepository;
ServerService = serverService; ServerService = serverService;
AuthorizeService = authorizeService;
} }
[HttpPost("{serverId:int}/start")] [HttpPost("{serverId:int}/start")]
@@ -54,14 +58,6 @@ public class ServerPowerController : Controller
await ServerService.Kill(server); await ServerService.Kill(server);
} }
[HttpPost("{serverId:int}/install")]
[Authorize]
public async Task Install([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Install(server);
}
private async Task<Server> GetServerById(int serverId) private async Task<Server> GetServerById(int serverId)
{ {
var server = await ServerRepository var server = await ServerRepository
@@ -72,11 +68,7 @@ public class ServerPowerController : Controller
if (server == null) if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
var userIdClaim = User.Claims.First(x => x.Type == "userId"); if (!await AuthorizeService.Authorize(User, server, permission => permission is { Name: "power", Type: ServerPermissionType.ReadWrite }))
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if (!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
return server; return server;

View File

@@ -1,3 +1,4 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -7,35 +8,49 @@ using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Extensions; using MoonlightServers.ApiServer.Extensions;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.ApiServer.Services; using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Responses.User.Allocations; using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Responses.Users.Servers; using MoonlightServers.Shared.Http.Responses.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Allocations;
namespace MoonlightServers.ApiServer.Http.Controllers.Client; namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController] [ApiController]
[Route("api/client/servers")] [Route("api/client/servers")]
public class ServersController : Controller public class ServersController : Controller
{ {
private readonly ServerService ServerService; private readonly ServerService ServerService;
private readonly DatabaseRepository<Server> ServerRepository; private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository; private readonly DatabaseRepository<ServerShare> ShareRepository;
private readonly NodeService NodeService; private readonly NodeService NodeService;
private readonly ServerAuthorizeService AuthorizeService;
public ServersController(DatabaseRepository<Server> serverRepository, NodeService nodeService, ServerService serverService, DatabaseRepository<User> userRepository) public ServersController(
DatabaseRepository<Server> serverRepository,
NodeService nodeService,
ServerService serverService,
ServerAuthorizeService authorizeService,
DatabaseRepository<ServerShare> shareRepository
)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
NodeService = nodeService; NodeService = nodeService;
ServerService = serverService; ServerService = serverService;
UserRepository = userRepository; AuthorizeService = authorizeService;
ShareRepository = shareRepository;
} }
[HttpGet] [HttpGet]
[Authorize]
public async Task<PagedData<ServerDetailResponse>> GetAll([FromQuery] int page, [FromQuery] int pageSize) public async Task<PagedData<ServerDetailResponse>> GetAll([FromQuery] int page, [FromQuery] int pageSize)
{ {
var userIdClaim = User.Claims.First(x => x.Type == "userId"); var userIdClaim = User.FindFirstValue("userId");
var userId = int.Parse(userIdClaim.Value);
if (string.IsNullOrEmpty(userIdClaim))
throw new HttpApiException("Only users are able to use this endpoint", 400);
var userId = int.Parse(userIdClaim);
var query = ServerRepository var query = ServerRepository
.Get() .Get()
@@ -53,6 +68,55 @@ public class ServersController : Controller
Name = x.Name, Name = x.Name,
NodeName = x.Node.Name, NodeName = x.Node.Name,
StarName = x.Star.Name, StarName = x.Star.Name,
Cpu = x.Cpu,
Memory = x.Memory,
Disk = x.Disk,
Allocations = x.Allocations.Select(y => new AllocationDetailResponse()
{
Id = y.Id,
Port = y.Port,
IpAddress = y.IpAddress
}).ToArray()
}).ToArray();
return new PagedData<ServerDetailResponse>()
{
Items = mappedItems,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : count / pageSize
};
}
[HttpGet("shared")]
public async Task<PagedData<ServerDetailResponse>> GetAllShared([FromQuery] int page, [FromQuery] int pageSize)
{
var userIdClaim = User.FindFirstValue("userId");
if (string.IsNullOrEmpty(userIdClaim))
throw new HttpApiException("Only users are able to use this endpoint", 400);
var userId = int.Parse(userIdClaim);
var query = ShareRepository
.Get()
.Include(x => x.Server)
.Where(x => x.UserId == userId)
.Select(x => x.Server);
var count = await query.CountAsync();
var items = await query.Skip(page * pageSize).Take(pageSize).ToArrayAsync();
var mappedItems = items.Select(x => new ServerDetailResponse()
{
Id = x.Id,
Name = x.Name,
NodeName = x.Node.Name,
StarName = x.Star.Name,
Cpu = x.Cpu,
Memory = x.Memory,
Disk = x.Disk,
Allocations = x.Allocations.Select(y => new AllocationDetailResponse() Allocations = x.Allocations.Select(y => new AllocationDetailResponse()
{ {
Id = y.Id, Id = y.Id,
@@ -72,7 +136,6 @@ public class ServersController : Controller
} }
[HttpGet("{serverId:int}")] [HttpGet("{serverId:int}")]
[Authorize]
public async Task<ServerDetailResponse> Get([FromRoute] int serverId) public async Task<ServerDetailResponse> Get([FromRoute] int serverId)
{ {
var server = await ServerRepository var server = await ServerRepository
@@ -85,11 +148,7 @@ public class ServersController : Controller
if (server == null) if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
var userIdClaim = User.Claims.First(x => x.Type == "userId"); if (!await AuthorizeService.Authorize(User, server))
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if(!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
return new ServerDetailResponse() return new ServerDetailResponse()
@@ -98,6 +157,9 @@ public class ServersController : Controller
Name = server.Name, Name = server.Name,
NodeName = server.Node.Name, NodeName = server.Node.Name,
StarName = server.Star.Name, StarName = server.Star.Name,
Cpu = server.Cpu,
Memory = server.Memory,
Disk = server.Disk,
Allocations = server.Allocations.Select(y => new AllocationDetailResponse() Allocations = server.Allocations.Select(y => new AllocationDetailResponse()
{ {
Id = y.Id, Id = y.Id,
@@ -108,10 +170,10 @@ public class ServersController : Controller
} }
[HttpGet("{serverId:int}/status")] [HttpGet("{serverId:int}/status")]
[Authorize]
public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId) public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId);
var status = await ServerService.GetStatus(server); var status = await ServerService.GetStatus(server);
return new ServerStatusResponse() return new ServerStatusResponse()
@@ -121,10 +183,12 @@ public class ServersController : Controller
} }
[HttpGet("{serverId:int}/ws")] [HttpGet("{serverId:int}/ws")]
[Authorize]
public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId) public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(
serverId,
permission => permission is { Name: "console", Type: >= ServerPermissionType.Read }
);
// TODO: Handle transparent node proxy // TODO: Handle transparent node proxy
@@ -147,10 +211,12 @@ public class ServersController : Controller
} }
[HttpGet("{serverId:int}/logs")] [HttpGet("{serverId:int}/logs")]
[Authorize]
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId) public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(
serverId,
permission => permission is { Name: "console", Type: >= ServerPermissionType.Read }
);
var logs = await ServerService.GetLogs(server); var logs = await ServerService.GetLogs(server);
@@ -160,7 +226,27 @@ public class ServersController : Controller
}; };
} }
private async Task<Server> GetServerById(int serverId) [HttpGet("{serverId:int}/stats")]
public async Task<ServerStatsResponse> GetStats([FromRoute] int serverId)
{
var server = await GetServerById(
serverId
);
var stats = await ServerService.GetStats(server);
return new ServerStatsResponse()
{
CpuUsage = stats.CpuUsage,
MemoryUsage = stats.MemoryUsage,
NetworkRead = stats.NetworkRead,
NetworkWrite = stats.NetworkWrite,
IoRead = stats.IoRead,
IoWrite = stats.IoWrite
};
}
private async Task<Server> GetServerById(int serverId, Func<ServerSharePermission, bool>? filter = null)
{ {
var server = await ServerRepository var server = await ServerRepository
.Get() .Get()
@@ -170,11 +256,7 @@ public class ServersController : Controller
if (server == null) if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
var userIdClaim = User.Claims.First(x => x.Type == "userId"); if (!await AuthorizeService.Authorize(User, server, filter))
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if(!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
return server; return server;

View File

@@ -0,0 +1,55 @@
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.Enums;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers")]
public class SettingsController : Controller
{
private readonly ServerService ServerService;
private readonly DatabaseRepository<Server> ServerRepository;
private readonly ServerAuthorizeService AuthorizeService;
public SettingsController(
ServerService serverService,
DatabaseRepository<Server> serverRepository,
ServerAuthorizeService authorizeService
)
{
ServerService = serverService;
ServerRepository = serverRepository;
AuthorizeService = authorizeService;
}
[HttpPost("{serverId:int}/install")]
[Authorize]
public async Task Install([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Install(server);
}
private async Task<Server> GetServerById(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);
if (!await AuthorizeService.Authorize(User, server, permission => permission is { Name: "settings", Type: ServerPermissionType.ReadWrite }))
throw new HttpApiException("No server with this id found", 404);
return server;
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers")]
public class SharesController : Controller
{
}

View File

@@ -6,6 +6,7 @@ using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services; using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Variables; using MoonlightServers.Shared.Http.Requests.Client.Servers.Variables;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Variables; using MoonlightServers.Shared.Http.Responses.Client.Servers.Variables;
@@ -14,27 +15,24 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize] [Authorize]
[ApiController] [ApiController]
[Route("api/client/servers")] [Route("api/client/servers")]
public class ServerVariablesController : Controller public class VariablesController : Controller
{ {
private readonly DatabaseRepository<Server> ServerRepository; private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository; private readonly ServerAuthorizeService AuthorizeService;
private readonly ServerService ServerService;
public ServerVariablesController( public VariablesController(
DatabaseRepository<Server> serverRepository, DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository, ServerAuthorizeService authorizeService
ServerService serverService
) )
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
UserRepository = userRepository; AuthorizeService = authorizeService;
ServerService = serverService;
} }
[HttpGet("{serverId:int}/variables")] [HttpGet("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse[]> Get([FromRoute] int serverId) public async Task<ServerVariableDetailResponse[]> Get([FromRoute] int serverId)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.Read);
return server.Star.Variables.Select(starVariable => return server.Star.Variables.Select(starVariable =>
{ {
@@ -53,11 +51,14 @@ public class ServerVariablesController : Controller
} }
[HttpPut("{serverId:int}/variables")] [HttpPut("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse> UpdateSingle([FromRoute] int serverId, [FromBody] UpdateServerVariableRequest request) public async Task<ServerVariableDetailResponse> UpdateSingle(
[FromRoute] int serverId,
[FromBody] UpdateServerVariableRequest request
)
{ {
// TODO: Handle filter // TODO: Handle filter
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
var serverVariable = server.Variables.FirstOrDefault(x => x.Key == request.Key); var serverVariable = server.Variables.FirstOrDefault(x => x.Key == request.Key);
var starVariable = server.Star.Variables.FirstOrDefault(x => x.Key == request.Key); var starVariable = server.Star.Variables.FirstOrDefault(x => x.Key == request.Key);
@@ -80,9 +81,12 @@ public class ServerVariablesController : Controller
} }
[HttpPatch("{serverId:int}/variables")] [HttpPatch("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse[]> Update([FromRoute] int serverId, [FromBody] UpdateServerVariableRangeRequest request) public async Task<ServerVariableDetailResponse[]> Update(
[FromRoute] int serverId,
[FromBody] UpdateServerVariableRangeRequest request
)
{ {
var server = await GetServerById(serverId); var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
foreach (var variable in request.Variables) foreach (var variable in request.Variables)
{ {
@@ -106,12 +110,17 @@ public class ServerVariablesController : Controller
return new ServerVariableDetailResponse() return new ServerVariableDetailResponse()
{ {
Key = starVariable.Key,
Value = serverVariable.Value,
Type = starVariable.Type,
Name = starVariable.Name,
Description = starVariable.Description,
Filter = starVariable.Filter
}; };
}).ToArray(); }).ToArray();
} }
private async Task<Server> GetServerById(int serverId) private async Task<Server> GetServerById(int serverId, ServerPermissionType type)
{ {
var server = await ServerRepository var server = await ServerRepository
.Get() .Get()
@@ -123,11 +132,8 @@ public class ServerVariablesController : Controller
if (server == null) if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
var userIdClaim = User.Claims.First(x => x.Type == "userId"); if (!await AuthorizeService.Authorize(User, server,
var userId = int.Parse(userIdClaim.Value); permission => permission.Name == "variables" && permission.Type >= type))
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if (!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
return server; return server;

View File

@@ -0,0 +1,9 @@
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Models;
public class ServerSharePermission
{
public string Name { get; set; }
public ServerPermissionType Type { get; set; }
}

View File

@@ -0,0 +1,129 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using MoonCore.Attributes;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Services;
[Scoped]
public class ServerAuthorizeService
{
private readonly IAuthorizationService AuthorizationService;
private readonly DatabaseRepository<ServerShare> ShareRepository;
public ServerAuthorizeService(
IAuthorizationService authorizationService,
DatabaseRepository<ServerShare> shareRepository
)
{
AuthorizationService = authorizationService;
ShareRepository = shareRepository;
}
public async Task<bool> Authorize(ClaimsPrincipal user, Server server, Func<ServerSharePermission, bool>? filter = null)
{
var userIdClaim = user.FindFirst("userId");
// User specific authorization
if (userIdClaim != null && await AuthorizeViaUser(userIdClaim, server, filter))
return true;
// Permission specific authorization
return await AuthorizeViaPermission(user);
}
private async Task<bool> AuthorizeViaUser(Claim userIdClaim, Server server, Func<ServerSharePermission, bool>? filter = null)
{
var userId = int.Parse(userIdClaim.Value);
if (server.OwnerId == userId)
return true;
var possibleShare = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.UserId == userId);
if (possibleShare == null)
return false;
// If no filter has been specified every server share is valid
// no matter which permission the share actually has
if (filter == null)
return true;
var permissionsOfShare = ParsePermissions(possibleShare.Permissions);
return permissionsOfShare.Any(filter);
}
private async Task<bool> AuthorizeViaPermission(ClaimsPrincipal user)
{
var authorizeResult = await AuthorizationService.AuthorizeAsync(
user,
"permissions:admin.servers.get"
);
return authorizeResult.Succeeded;
}
private ServerSharePermission[] ParsePermissions(string permissionsString)
{
var result = new List<ServerSharePermission>();
var permissions = permissionsString.Split(';', StringSplitOptions.RemoveEmptyEntries);
foreach (var permission in permissions)
{
var permissionParts = permission.Split(':', StringSplitOptions.RemoveEmptyEntries);
// Skipped malformed permission parts
if(permissionParts.Length != 2)
continue;
if(!Enum.TryParse(permissionParts[1], true, out ServerPermissionType permissionType))
continue;
result.Add(new()
{
Name = permissionParts[0],
Type = permissionType
});
}
return result.ToArray();
}
private bool CheckSharePermission(ServerShare share, string permission, ServerPermissionType type)
{
if (string.IsNullOrEmpty(share.Permissions))
return false;
var permissions = share.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries);
foreach (var sharePermission in permissions)
{
if (!sharePermission.StartsWith(permission))
continue;
var typeParts = sharePermission.Split(':', StringSplitOptions.RemoveEmptyEntries);
// Missing permission type
if (typeParts.Length != 2)
return false;
// Parse type id
if (!int.TryParse(typeParts[1], out var typeId))
return false; // Malformed
var requiredId = (int)type;
return typeId >= requiredId;
}
return false;
}
}

View File

@@ -130,6 +130,19 @@ public class ServerService
} }
} }
public async Task<ServerStatsResponse> GetStats(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
return await apiClient.GetJson<ServerStatsResponse>($"api/servers/{server.Id}/stats");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
#region Helpers #region Helpers
public bool IsAllowedToAccess(User user, Server server) public bool IsAllowedToAccess(User user, Server server)

View File

@@ -64,4 +64,25 @@ public class ServersController : Controller
Messages = messages Messages = messages
}; };
} }
[HttpGet("{serverId:int}/stats")]
public Task<ServerStatsResponse> GetStats([FromRoute] int serverId)
{
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()
{
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

@@ -8,6 +8,8 @@ namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class StatsSubSystem : ServerSubSystem public class StatsSubSystem : ServerSubSystem
{ {
public ServerStats CurrentStats { get; private set; }
private readonly DockerClient DockerClient; private readonly DockerClient DockerClient;
private readonly IHubContext<ServerWebSocketHub> HubContext; private readonly IHubContext<ServerWebSocketHub> HubContext;
@@ -20,6 +22,8 @@ public class StatsSubSystem : ServerSubSystem
{ {
DockerClient = dockerClient; DockerClient = dockerClient;
HubContext = hubContext; HubContext = hubContext;
CurrentStats = new();
} }
public Task Attach(string containerId) public Task Attach(string containerId)
@@ -44,6 +48,9 @@ public class StatsSubSystem : ServerSubSystem
{ {
var stats = ConvertToStats(response); var stats = ConvertToStats(response);
// Update current stats for usage of other components
CurrentStats = stats;
await HubContext.Clients await HubContext.Clients
.Group(Configuration.Id.ToString()) .Group(Configuration.Id.ToString())
.SendAsync("StatsUpdated", stats); .SendAsync("StatsUpdated", stats);
@@ -66,6 +73,9 @@ public class StatsSubSystem : ServerSubSystem
} }
} }
// Reset current stats
CurrentStats = new();
Logger.LogDebug("Stopped fetching container stats"); Logger.LogDebug("Stopped fetching container stats");
}); });
@@ -76,20 +86,16 @@ public class StatsSubSystem : ServerSubSystem
{ {
var result = new ServerStats(); var result = new ServerStats();
// When killed this field will be null so we just return
if (response.CPUStats.CPUUsage == null)
return result;
#region CPU #region CPU
if(response.CPUStats is { CPUUsage.PercpuUsage: not null }) // Sometimes some values are just null >:/ if(response.CPUStats != null && response.PreCPUStats.CPUUsage != null) // Sometimes some values are just null >:/
{ {
var cpuDelta = (float)response.CPUStats.CPUUsage.TotalUsage - response.PreCPUStats.CPUUsage.TotalUsage; var cpuDelta = (float)response.CPUStats.CPUUsage.TotalUsage - response.PreCPUStats.CPUUsage.TotalUsage;
var cpuSystemDelta = (float)response.CPUStats.SystemUsage - response.PreCPUStats.SystemUsage; var cpuSystemDelta = (float)response.CPUStats.SystemUsage - response.PreCPUStats.SystemUsage;
var cpuCoreCount = (int)response.CPUStats.OnlineCPUs; var cpuCoreCount = (int)response.CPUStats.OnlineCPUs;
if (cpuCoreCount == 0) if (cpuCoreCount == 0 && response.CPUStats.CPUUsage.PercpuUsage != null)
cpuCoreCount = response.CPUStats.CPUUsage.PercpuUsage.Count; cpuCoreCount = response.CPUStats.CPUUsage.PercpuUsage.Count;
var cpuPercent = 0f; var cpuPercent = 0f;
@@ -115,11 +121,14 @@ public class StatsSubSystem : ServerSubSystem
#region Network #region Network
if (response.Networks != null)
{
foreach (var network in response.Networks) foreach (var network in response.Networks)
{ {
result.NetworkRead += network.Value.RxBytes; result.NetworkRead += network.Value.RxBytes;
result.NetworkWrite += network.Value.TxBytes; result.NetworkWrite += network.Value.TxBytes;
} }
}
#endregion #endregion

View File

@@ -92,6 +92,14 @@ public class StorageSubSystem : ServerSubSystem
public async Task Reinitialize() public async Task Reinitialize()
{ {
if (IsInitialized && StateMachine.State != ServerState.Offline)
{
throw new HttpApiException(
"Unable to reinitialize storage sub system while the server is not offline",
400
);
}
IsInitialized = false; IsInitialized = false;
await EnsureRuntimeVolumeCreated(); await EnsureRuntimeVolumeCreated();
@@ -304,6 +312,22 @@ public class StorageSubSystem : ServerSubSystem
{ {
var expectedSize = ByteConverter.FromMegaBytes(Configuration.Disk).Bytes; var expectedSize = ByteConverter.FromMegaBytes(Configuration.Disk).Bytes;
// If the disk size matches, we are done here
if (expectedSize == existingDiskInfo.Length)
{
Logger.LogDebug("Virtual disk size matches expected size");
return;
}
// We cant resize while the server is running as this would lead to possible file corruptions
// and crashes of the software the server is running
if (StateMachine.State != ServerState.Offline)
{
Logger.LogDebug("Skipping disk resizing while server is not offline");
await ConsoleSubSystem.WriteMoonlight("Skipping disk resizing as the server is not offline");
return;
}
if (expectedSize > existingDiskInfo.Length) if (expectedSize > existingDiskInfo.Length)
{ {
Logger.LogDebug("Detected smaller disk size as expected. Resizing now"); Logger.LogDebug("Detected smaller disk size as expected. Resizing now");

View File

@@ -78,12 +78,14 @@ public class ServerService : IHostedLifecycleService
Type[] subSystems = Type[] subSystems =
[ [
// The restore sub system needs to be on top in order for the state machine having the
// correct state when all other sub systems initialize
typeof(RestoreSubSystem),
typeof(ProvisionSubSystem), typeof(ProvisionSubSystem),
typeof(StorageSubSystem), typeof(StorageSubSystem),
typeof(DebugSubSystem), typeof(DebugSubSystem),
typeof(ShutdownSubSystem), typeof(ShutdownSubSystem),
typeof(ConsoleSubSystem), typeof(ConsoleSubSystem),
typeof(RestoreSubSystem),
typeof(OnlineDetectionService), typeof(OnlineDetectionService),
typeof(InstallationSubSystem), typeof(InstallationSubSystem),
typeof(StatsSubSystem) typeof(StatsSubSystem)

View File

@@ -0,0 +1,11 @@
namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
public class ServerStatsResponse
{
public double CpuUsage { get; set; }
public ulong MemoryUsage { get; set; }
public ulong NetworkRead { get; set; }
public ulong NetworkWrite { get; set; }
public ulong IoRead { get; set; }
public ulong IoWrite { get; set; }
}

View File

@@ -1,7 +1,7 @@
using MoonlightServers.Frontend.Interfaces; using MoonlightServers.Frontend.Interfaces;
using MoonlightServers.Frontend.Models; using MoonlightServers.Frontend.Models;
using MoonlightServers.Frontend.UI.Components.Servers.ServerTabs; using MoonlightServers.Frontend.UI.Components.Servers.ServerTabs;
using MoonlightServers.Shared.Http.Responses.Users.Servers; using MoonlightServers.Shared.Http.Responses.Client.Servers;
namespace MoonlightServers.Frontend.Implementations; namespace MoonlightServers.Frontend.Implementations;

View File

@@ -1,5 +1,5 @@
using MoonlightServers.Frontend.Models; using MoonlightServers.Frontend.Models;
using MoonlightServers.Shared.Http.Responses.Users.Servers; using MoonlightServers.Shared.Http.Responses.Client.Servers;
namespace MoonlightServers.Frontend.Interfaces; namespace MoonlightServers.Frontend.Interfaces;

View File

@@ -2,8 +2,8 @@ using MoonCore.Attributes;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCore.Models; using MoonCore.Models;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Variables; using MoonlightServers.Shared.Http.Requests.Client.Servers.Variables;
using MoonlightServers.Shared.Http.Responses.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Variables; using MoonlightServers.Shared.Http.Responses.Client.Servers.Variables;
using MoonlightServers.Shared.Http.Responses.Users.Servers;
namespace MoonlightServers.Frontend.Services; namespace MoonlightServers.Frontend.Services;
@@ -45,6 +45,13 @@ public class ServerService
); );
} }
public async Task<ServerStatsResponse> GetStats(int serverId)
{
return await HttpApiClient.GetJson<ServerStatsResponse>(
$"api/client/servers/{serverId}/stats"
);
}
public async Task<ServerWebSocketResponse> GetWebSocket(int serverId) public async Task<ServerWebSocketResponse> GetWebSocket(int serverId)
{ {
return await HttpApiClient.GetJson<ServerWebSocketResponse>( return await HttpApiClient.GetJson<ServerWebSocketResponse>(

View File

@@ -1,6 +1,7 @@
@using MoonCore.Helpers
@using MoonlightServers.Frontend.Services @using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Enums @using MoonlightServers.Shared.Enums
@using MoonlightServers.Shared.Http.Responses.Users.Servers @using MoonlightServers.Shared.Http.Responses.Client.Servers
@inject ServerService ServerService @inject ServerService ServerService
@inject ILogger<ServerCard> Logger @inject ILogger<ServerCard> Logger
@@ -57,7 +58,7 @@
<i class="icon-cpu"></i> <i class="icon-cpu"></i>
</div> </div>
<div class="ms-3">56,8%</div> <div class="ms-3">@(Stats.CpuUsage)%</div>
</div> </div>
<div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row"> <div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row">
@@ -65,7 +66,7 @@
<i class="icon-memory-stick"></i> <i class="icon-memory-stick"></i>
</div> </div>
<div class="ms-3">4,2 GB / 8 GB</div> <div class="ms-3">@(Formatter.FormatSize(Stats.MemoryUsage)) / @(Formatter.FormatSize(ByteConverter.FromMegaBytes(Server.Memory).Bytes))</div>
</div> </div>
<div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row"> <div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row">
@@ -147,6 +148,7 @@
[Parameter] public ServerDetailResponse Server { get; set; } [Parameter] public ServerDetailResponse Server { get; set; }
private ServerStatusResponse Status; private ServerStatusResponse Status;
private ServerStatsResponse Stats;
private bool IsFailed = false; private bool IsFailed = false;
private bool IsLoaded = false; private bool IsLoaded = false;
@@ -159,6 +161,7 @@
try try
{ {
Status = await ServerService.GetStatus(Server.Id); Status = await ServerService.GetStatus(Server.Id);
Stats = await ServerService.GetStats(Server.Id);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -1,7 +1,7 @@
@using Microsoft.AspNetCore.SignalR.Client @using Microsoft.AspNetCore.SignalR.Client
@using MoonlightServers.Frontend.UI.Views.Client @using MoonlightServers.Frontend.UI.Views.Client
@using MoonlightServers.Shared.Enums @using MoonlightServers.Shared.Enums
@using MoonlightServers.Shared.Http.Responses.Users.Servers @using MoonlightServers.Shared.Http.Responses.Client.Servers
@code @code
{ {

View File

@@ -1,4 +1,4 @@
@using MoonlightServers.Shared.Http.Responses.Users.Servers @using MoonlightServers.Shared.Http.Responses.Client.Servers
@{ @{
var gradient = Status switch var gradient = Status switch
{ {

View File

@@ -4,11 +4,19 @@
@using MoonCore.Blazor.Tailwind.Components @using MoonCore.Blazor.Tailwind.Components
@using MoonCore.Models @using MoonCore.Models
@using MoonlightServers.Frontend.Services @using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Http.Responses.Users.Servers @using MoonlightServers.Shared.Http.Responses.Client.Servers
@inject ServerService ServerService @inject ServerService ServerService
<LazyLoader Load="Load"> <LazyLoader Load="Load">
@if (Servers.Length == 0)
{
<IconAlert Title="No servers found" Color="text-primary" Icon="icon-search">
There are no servers linked to your account
</IconAlert>
}
else
{
<div class="flex flex-col gap-y-5"> <div class="flex flex-col gap-y-5">
@* Folder design idea @* Folder design idea
<div class="w-full bg-gray-800 px-5 py-3.5 rounded-xl"> <div class="w-full bg-gray-800 px-5 py-3.5 rounded-xl">
@@ -33,6 +41,7 @@
<ServerCard Server="server"/> <ServerCard Server="server"/>
} }
</div> </div>
}
</LazyLoader> </LazyLoader>
@code @code

View File

@@ -3,7 +3,6 @@
@using Microsoft.AspNetCore.Http.Connections @using Microsoft.AspNetCore.Http.Connections
@using Microsoft.AspNetCore.SignalR.Client @using Microsoft.AspNetCore.SignalR.Client
@using MoonlightServers.Shared.Http.Responses.Users.Servers
@using MoonCore.Blazor.Tailwind.Components @using MoonCore.Blazor.Tailwind.Components
@using MoonCore.Exceptions @using MoonCore.Exceptions
@using MoonCore.Helpers @using MoonCore.Helpers
@@ -11,6 +10,7 @@
@using MoonlightServers.Frontend.Models @using MoonlightServers.Frontend.Models
@using MoonlightServers.Frontend.Services @using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Enums @using MoonlightServers.Shared.Enums
@using MoonlightServers.Shared.Http.Responses.Client.Servers
@inject ServerService ServerService @inject ServerService ServerService
@inject NavigationManager Navigation @inject NavigationManager Navigation

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Shared.Enums;
public enum ServerPermissionType
{
Read = 0,
ReadWrite = 1
}

View File

@@ -1,4 +1,4 @@
namespace MoonlightServers.Shared.Http.Responses.User.Allocations; namespace MoonlightServers.Shared.Http.Responses.Client.Servers.Allocations;
public class AllocationDetailResponse public class AllocationDetailResponse
{ {

View File

@@ -1,6 +1,6 @@
using MoonlightServers.Shared.Http.Responses.User.Allocations; using MoonlightServers.Shared.Http.Responses.Client.Servers.Allocations;
namespace MoonlightServers.Shared.Http.Responses.Users.Servers; namespace MoonlightServers.Shared.Http.Responses.Client.Servers;
public class ServerDetailResponse public class ServerDetailResponse
{ {
@@ -8,6 +8,10 @@ public class ServerDetailResponse
public string Name { get; set; } public string Name { get; set; }
public int Cpu { get; set; }
public int Memory { get; set; }
public int Disk { get; set; }
public string NodeName { get; set; } public string NodeName { get; set; }
public string StarName { get; set; } public string StarName { get; set; }

View File

@@ -1,4 +1,4 @@
namespace MoonlightServers.Shared.Http.Responses.Users.Servers; namespace MoonlightServers.Shared.Http.Responses.Client.Servers;
public class ServerLogsResponse public class ServerLogsResponse
{ {

View File

@@ -0,0 +1,11 @@
namespace MoonlightServers.Shared.Http.Responses.Client.Servers;
public class ServerStatsResponse
{
public double CpuUsage { get; set; }
public ulong MemoryUsage { get; set; }
public ulong NetworkRead { get; set; }
public ulong NetworkWrite { get; set; }
public ulong IoRead { get; set; }
public ulong IoWrite { get; set; }
}

View File

@@ -1,6 +1,6 @@
using MoonlightServers.Shared.Enums; using MoonlightServers.Shared.Enums;
namespace MoonlightServers.Shared.Http.Responses.Users.Servers; namespace MoonlightServers.Shared.Http.Responses.Client.Servers;
public class ServerStatusResponse public class ServerStatusResponse
{ {

View File

@@ -1,4 +1,4 @@
namespace MoonlightServers.Shared.Http.Responses.Users.Servers; namespace MoonlightServers.Shared.Http.Responses.Client.Servers;
public class ServerWebSocketResponse public class ServerWebSocketResponse
{ {

View File

@@ -0,0 +1,9 @@
namespace MoonlightServers.Shared.Http.Responses.Client.Servers.Shares;
public class ServerShareResponse
{
public int Id { get; set; }
public string Email { get; set; }
public string Permissions { get; set; }
}

View File

@@ -14,4 +14,8 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="Http\Responses\Users\Servers\" />
</ItemGroup>
</Project> </Project>