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

AdminLayout

+ +@code { + +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor b/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor new file mode 100644 index 0000000..79d9086 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor @@ -0,0 +1,289 @@ +@using Moonlight.Features.ServiceManagement.Entities +@using Moonlight.Features.ServiceManagement.Models.Abstractions +@using Moonlight.Features.Servers.Entities +@using Moonlight.Features.Servers.Services +@using Moonlight.Core.Repositories +@using Moonlight.Core.Services.Interop +@using Moonlight.Features.Servers.Models.Abstractions +@using Moonlight.Features.Servers.Models.Enums +@using Microsoft.EntityFrameworkCore +@using Moonlight.Core.Helpers +@using Moonlight.Features.Servers.UI.Components + +@inject Repository ServerRepository +@inject ServerService ServerService +@inject ToastService ToastService + +@implements IDisposable + + +
+
+
+
+ + @(Service.Nickname ?? $"Service {Service.Id}") + + + @(Server.Image.Name) + +
+
+
+
+ @{ + var color = "secondary"; + + switch (Meta.State) + { + case ServerState.Stopping: + color = "warning"; + break; + + case ServerState.Starting: + color = "warning"; + break; + + case ServerState.Offline: + color = "danger"; + break; + + case ServerState.Online: + color = "success"; + break; + + case ServerState.Installing: + color = "primary"; + break; + + case ServerState.Join2Start: + color = "info"; + break; + } + } + + + + @(Meta.State) + (@(Formatter.FormatUptime(DateTime.UtcNow - Meta.LastChangeTimestamp))) + +
+
+
+ + + @(Server.Node.Fqdn):@(Server.MainAllocation.Port) + +
+
+ + + 188.75.252.37:10324 + +
+
+
+
+
+
+ @if (Meta.State == ServerState.Offline) + { + + + + } + else + { + + } + + @if (Meta.State == ServerState.Offline || Meta.State == ServerState.Installing) + { + + } + else + { + + + + } + + @if (Meta.State == ServerState.Offline || Meta.State == ServerState.Installing) + { + + } + else + { + + + + } +
+
+
+ +
+ +
+ @if (IsInstalling) + { +
+ +
+ } + else + { + + + + + @foreach (var uiPage in ViewContext.Pages) + { + + @uiPage.Component + + } + + + + + } +
+
+ +@code +{ + [Parameter] + public Service Service { get; set; } + + [Parameter] + public ServiceViewContext ViewContext { get; set; } + + [Parameter] + public string? Route { get; set; } + + private Server Server; + private ServerMeta Meta; + private CancellationTokenSource BackgroundCancel = new(); + + private Terminal? InstallTerminal; + private bool IsInstalling = false; + + private async Task Load(LazyLoader lazyLoader) + { + await lazyLoader.SetText("Loading server information"); + + Server = ServerRepository + .Get() + .Include(x => x.Image) + .Include(x => x.Node) + .Include(x => x.Variables) + .Include(x => x.Allocations) + .Include(x => x.MainAllocation) + .First(x => x.Service.Id == Service.Id); + + /* + + // Load meta and setup event handlers + Meta = await ServerService.Meta.Get(Server); + Meta.OnStateChanged += async Task () => + { + await InvokeAsync(StateHasChanged); + + // Change from offline to installing + // This will trigger the initialisation of the install view + if (Meta.State == ServerState.Installing && !IsInstalling) + { + IsInstalling = true; + + // After this call, we should have access to the install terminal reference + await InvokeAsync(StateHasChanged); + + Meta.OnConsoleMessage += OnInstallConsoleMessage; + } + // Change from installing to offline + // This will trigger the destruction of the install view + else if (Meta.State == ServerState.Offline && IsInstalling) + { + IsInstalling = false; + + Meta.OnConsoleMessage -= OnInstallConsoleMessage; + + // After this call, the install terminal will disappear + await InvokeAsync(StateHasChanged); + + await ToastService.Info("Server installation complete"); + } + }; + + // Send console subscription and add auto resubscribe for it + await ServerService.Console.Subscribe(Server); + + // We need this to revalidate to the daemon that we are still interested + // in the console logs. By default the expiration time is 15 minutes from last + // subscription so every 10 minutes should ensure we are subscribed + Task.Run(async () => + { + while (!BackgroundCancel.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMinutes(10)); + await ServerService.Console.Subscribe(Server); + } + }); + + // In order to update the timer correctly, we are calling a re + Task.Run(async () => + { + while (!BackgroundCancel.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + await InvokeAsync(StateHasChanged); + } + }); + + */ + } + + private async Task OnInstallConsoleMessage(string message) + { + if(InstallTerminal != null) + await InstallTerminal.WriteLine(message); + } + + private async Task Start() => await ServerService.SendPowerAction(Server, PowerAction.Start); + + private async Task Stop() => await ServerService.SendPowerAction(Server, PowerAction.Stop); + + private async Task Kill() => await ServerService.SendPowerAction(Server, PowerAction.Kill); + + public void Dispose() + { + BackgroundCancel.Cancel(); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Index.razor b/Moonlight/Features/Servers/UI/Views/Admin/Index.razor new file mode 100644 index 0000000..9a04626 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Views/Admin/Index.razor @@ -0,0 +1,72 @@ +@page "/admin/servers" + +@using Moonlight.Core.Extensions.Attributes +@using Moonlight.Core.Models.Enums +@using Moonlight.Core.Repositories +@using Moonlight.Features.Servers.UI.Components +@using Moonlight.Features.Servers.Entities +@using Moonlight.Features.Servers.Models.Forms.Admin +@using Microsoft.EntityFrameworkCore +@using Moonlight.Core.Exceptions +@using Moonlight.Core.Helpers +@using BlazorTable + +@attribute [RequirePermission(Permission.AdminServers)] + +@inject Repository NodeRepository + + + + + + + + + + + + + + + + +@code +{ + private ServerNode[] Load(Repository repository) + { + return repository.Get().ToArray(); + } + + private Task ValidateAdd(ServerNode node) + { + // Generate token + node.Token = Formatter.GenerateString(32); + + return Task.CompletedTask; + } + + private Task ValidateDelete(ServerNode n) + { + var nodeHasAllocations = NodeRepository + .Get() + .Include(x => x.Allocations) + .First(x => x.Id == n.Id) + .Allocations + .Any(); + + if (nodeHasAllocations) + throw new DisplayException("The node still has allocations. Delete them in order to delete the node"); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Nodes/View.razor b/Moonlight/Features/Servers/UI/Views/Admin/Nodes/View.razor new file mode 100644 index 0000000..9ab860f --- /dev/null +++ b/Moonlight/Features/Servers/UI/Views/Admin/Nodes/View.razor @@ -0,0 +1,7 @@ +@page "/admin/servers/nodes/{Id:int}" + +@code +{ + [Parameter] + public int Id { get; set; } +} diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index c39c194..fe7432f 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -34,13 +34,8 @@ - - - - - @@ -68,6 +63,7 @@ + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 91cbd66..8af7466 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -1,4 +1,5 @@ using BlazorTable; +using Microsoft.AspNetCore.WebSockets; using Moonlight.Core.Database; using Moonlight.Core.Actions.Dummy; using Moonlight.Core.Database; @@ -13,6 +14,7 @@ using Moonlight.Core.Services.Users; using Moonlight.Core.Services.Utils; using Moonlight.Features.Advertisement.Services; using Moonlight.Features.Community.Services; +using Moonlight.Features.Servers.Actions; using Moonlight.Features.Servers.Http.Middleware; using Moonlight.Features.Servers.Services; using Moonlight.Features.ServiceManagement.Entities.Enums; @@ -73,6 +75,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Services / Store builder.Services.AddScoped(); @@ -107,6 +110,7 @@ builder.Services.AddScoped(); // Services / Servers builder.Services.AddSingleton(); +builder.Services.AddSingleton(); // Services builder.Services.AddScoped(); @@ -135,6 +139,7 @@ var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); +app.UseWebSockets(); app.UseMiddleware(); @@ -150,7 +155,7 @@ moonlightService.Application = app; moonlightService.LogPath = logPath; var serviceService = app.Services.GetRequiredService(); -serviceService.Register(ServiceType.Server); +serviceService.Register(ServiceType.Server); await pluginService.RunPrePost(app); diff --git a/Moonlight/wwwroot/js/moonlight.js b/Moonlight/wwwroot/js/moonlight.js index 4a7a659..60d7c71 100644 --- a/Moonlight/wwwroot/js/moonlight.js +++ b/Moonlight/wwwroot/js/moonlight.js @@ -169,5 +169,38 @@ window.moonlight = { let editor = document.getElementById(id).ckeditorInstance; editor.setData(data); } + }, + clipboard: { + copy: function (text) { + if (!navigator.clipboard) { + var textArea = document.createElement("textarea"); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.top = "0"; + textArea.style.left = "0"; + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + var successful = document.execCommand('copy'); + var msg = successful ? 'successful' : 'unsuccessful'; + } catch (err) { + console.error('Fallback: Oops, unable to copy', err); + } + + document.body.removeChild(textArea); + return; + } + navigator.clipboard.writeText(text).then(function () { + }, + function (err) { + console.error('Async: Could not copy text: ', err); + } + ); + } } } \ No newline at end of file From 99a7d7bd73fcf361884c9f2fe730eeae595cd618 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 28 Jan 2024 13:58:23 +0100 Subject: [PATCH 03/37] Started implementing image editor and parse config editor --- ...edMissingPropertyInServerImage.Designer.cs | 1040 +++++++++++++++++ ...64420_FixedMissingPropertyInServerImage.cs | 48 + .../Migrations/DataContextModelSnapshot.cs | 14 + Moonlight/Core/Helpers/ValidatorHelper.cs | 33 + .../UI/Components/Forms/AutoListCrud.razor | 244 ++++ .../Features/Servers/Entities/ServerImage.cs | 2 +- .../Forms/Admin/Images/ParseConfigForm.cs | 12 + .../Admin/Images/ParseConfigOptionForm.cs | 10 + .../Servers/Models/ServerParseConfig.cs | 8 + .../Servers/UI/Components/ImageEditor.razor | 154 +++ .../UI/Components/ParseConfigEditor.razor | 156 +++ .../Servers/UI/Views/Admin/Images/Index.razor | 129 ++ .../Servers/UI/Views/Admin/Images/New.razor | 10 + 13 files changed, 1859 insertions(+), 1 deletion(-) create mode 100644 Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.Designer.cs create mode 100644 Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.cs create mode 100644 Moonlight/Core/Helpers/ValidatorHelper.cs create mode 100644 Moonlight/Core/UI/Components/Forms/AutoListCrud.razor create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/Images/ParseConfigForm.cs create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/Images/ParseConfigOptionForm.cs create mode 100644 Moonlight/Features/Servers/Models/ServerParseConfig.cs create mode 100644 Moonlight/Features/Servers/UI/Components/ImageEditor.razor create mode 100644 Moonlight/Features/Servers/UI/Components/ParseConfigEditor.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Images/New.razor diff --git a/Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.Designer.cs b/Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.Designer.cs new file mode 100644 index 0000000..5be6d5e --- /dev/null +++ b/Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.Designer.cs @@ -0,0 +1,1040 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.Core.Database; + +#nullable disable + +namespace Moonlight.Core.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240127164420_FixedMissingPropertyInServerImage")] + partial class FixedMissingPropertyInServerImage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.WordFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WordFilters"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Cpu") + .HasColumnType("INTEGER"); + + b.Property("Disk") + .HasColumnType("INTEGER"); + + b.Property("DockerImageIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("MainAllocationId") + .HasColumnType("INTEGER"); + + b.Property("Memory") + .HasColumnType("INTEGER"); + + b.Property("NodeId") + .HasColumnType("INTEGER"); + + b.Property("OverrideStartupCommand") + .HasColumnType("TEXT"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("ServerNodeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("ServerNodeId"); + + b.ToTable("ServerAllocations"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoPull") + .HasColumnType("INTEGER"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerImageId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerImageId"); + + b.ToTable("ServerDockerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllocationsNeeded") + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DefaultDockerImageIndex") + .HasColumnType("INTEGER"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InstallShell") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OnlineDetection") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParseConfigurations") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartupCommand") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdateUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ServerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowUserToEdit") + .HasColumnType("INTEGER"); + + b.Property("AllowUserToView") + .HasColumnType("INTEGER"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerImageId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServerImageId"); + + b.ToTable("ServerImageVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FtpPort") + .HasColumnType("INTEGER"); + + b.Property("HttpPort") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UseSsl") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerNodes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("Suspended") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.ServiceShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.HasIndex("UserId"); + + b.ToTable("ServiceShares"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CouponId"); + + b.HasIndex("UserId"); + + b.ToTable("CouponUses"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GiftCodeId"); + + b.HasIndex("UserId"); + + b.ToTable("GiftCodeUses"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.Features.Theming.Entities.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CssUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("JsUrl") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Themes"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Open") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("Tries") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsSupport") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("TicketId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketMessages"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostComment", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Community.Entities.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.PostLike", b => + { + b.HasOne("Moonlight.Features.Community.Entities.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.Core.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerAllocation", "MainAllocation") + .WithMany() + .HasForeignKey("MainAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Node"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", null) + .WithMany("Allocations") + .HasForeignKey("ServerNodeId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("DockerImages") + .HasForeignKey("ServerImageId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("Variables") + .HasForeignKey("ServerImageId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.StoreSystem.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.ServiceShare", b => + { + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", null) + .WithMany("Shares") + .HasForeignKey("ServiceId"); + + b.HasOne("Moonlight.Core.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.CouponUse", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("CouponUses") + .HasForeignKey("UserId"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.GiftCodeUse", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.GiftCode", "GiftCode") + .WithMany() + .HasForeignKey("GiftCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("GiftCodeUses") + .HasForeignKey("UserId"); + + b.Navigation("GiftCode"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Product", b => + { + b.HasOne("Moonlight.Features.StoreSystem.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Moonlight.Features.StoreSystem.Entities.Transaction", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", null) + .WithMany("Transactions") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.ServiceManagement.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId"); + + b.Navigation("Creator"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.TicketMessage", b => + { + b.HasOne("Moonlight.Core.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.HasOne("Moonlight.Features.Ticketing.Entities.Ticket", null) + .WithMany("Messages") + .HasForeignKey("TicketId"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => + { + b.Navigation("CouponUses"); + + b.Navigation("GiftCodeUses"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Moonlight.Features.Community.Entities.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.Features.ServiceManagement.Entities.Service", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("Moonlight.Features.Ticketing.Entities.Ticket", b => + { + b.Navigation("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.cs b/Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.cs new file mode 100644 index 0000000..f656bc9 --- /dev/null +++ b/Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.Core.Database.Migrations +{ + /// + public partial class FixedMissingPropertyInServerImage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ServerImageId", + table: "ServerImageVariables", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ServerImageVariables_ServerImageId", + table: "ServerImageVariables", + column: "ServerImageId"); + + migrationBuilder.AddForeignKey( + name: "FK_ServerImageVariables_ServerImages_ServerImageId", + table: "ServerImageVariables", + column: "ServerImageId", + principalTable: "ServerImages", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ServerImageVariables_ServerImages_ServerImageId", + table: "ServerImageVariables"); + + migrationBuilder.DropIndex( + name: "IX_ServerImageVariables_ServerImageId", + table: "ServerImageVariables"); + + migrationBuilder.DropColumn( + name: "ServerImageId", + table: "ServerImageVariables"); + } + } +} diff --git a/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs index fbc8ffb..cd9d4e1 100644 --- a/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs @@ -353,8 +353,13 @@ namespace Moonlight.Core.Database.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("ServerImageId") + .HasColumnType("INTEGER"); + b.HasKey("Id"); + b.HasIndex("ServerImageId"); + b.ToTable("ServerImageVariables"); }); @@ -856,6 +861,13 @@ namespace Moonlight.Core.Database.Migrations .HasForeignKey("ServerImageId"); }); + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("Variables") + .HasForeignKey("ServerImageId"); + }); + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => { b.HasOne("Moonlight.Features.Servers.Entities.Server", null) @@ -1001,6 +1013,8 @@ namespace Moonlight.Core.Database.Migrations modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => { b.Navigation("DockerImages"); + + b.Navigation("Variables"); }); modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => diff --git a/Moonlight/Core/Helpers/ValidatorHelper.cs b/Moonlight/Core/Helpers/ValidatorHelper.cs new file mode 100644 index 0000000..35614cf --- /dev/null +++ b/Moonlight/Core/Helpers/ValidatorHelper.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using Moonlight.Core.Exceptions; + +namespace Moonlight.Core.Helpers; + +public class ValidatorHelper +{ + public static Task Validate(object objectToValidate) + { + var context = new ValidationContext(objectToValidate, null, null); + var results = new List(); + + var isValid = Validator.TryValidateObject(objectToValidate, context, results, true); + + if (!isValid) + { + var errorMsg = "Unknown form error"; + + if (results.Any()) + errorMsg = results.First().ErrorMessage ?? errorMsg; + + throw new DisplayException(errorMsg); + } + + return Task.CompletedTask; + } + + public static async Task ValidateRange(IEnumerable objectToValidate) + { + foreach (var o in objectToValidate) + await Validate(o); + } +} \ No newline at end of file diff --git a/Moonlight/Core/UI/Components/Forms/AutoListCrud.razor b/Moonlight/Core/UI/Components/Forms/AutoListCrud.razor new file mode 100644 index 0000000..3945be9 --- /dev/null +++ b/Moonlight/Core/UI/Components/Forms/AutoListCrud.razor @@ -0,0 +1,244 @@ +@using BlazorTable +@using System.Linq.Expressions +@using Mappy.Net +@using Moonlight.Core.Services.Interop + +@typeparam TItem where TItem : class +@typeparam TRootItem where TRootItem : class +@typeparam TCreateForm +@typeparam TUpdateForm + +@inject ToastService ToastService + +
+
+

@(Title)

+
+ @Toolbar + +
+
+
+ + + @View + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +@code +{ + [Parameter] + public string Title { get; set; } = ""; + + [Parameter] + public TRootItem RootItem { get; set; } + + [Parameter] + public Func> Field { get; set; } + + [Parameter] + public RenderFragment View { get; set; } + + [Parameter] + public RenderFragment Toolbar { get; set; } + + [Parameter] + public Func? ValidateAdd { get; set; } + + [Parameter] + public Func? ValidateUpdate { get; set; } + + [Parameter] + public Func? ValidateDelete { get; set; } + + private TItem[] Items; + private TCreateForm CreateForm; + private TUpdateForm UpdateForm; + private TItem ItemToUpdate; + private TItem ItemToDelete; + + private SmartModal CreateModal; + private SmartModal UpdateModal; + private SmartModal DeleteModal; + + private Expression> IdExpression; + private LazyLoader LazyLoader; + + protected override void OnInitialized() + { + if (Field == null) + throw new ArgumentNullException(nameof(Field)); + + CreateForm = Activator.CreateInstance()!; + UpdateForm = Activator.CreateInstance()!; + + IdExpression = CreateExpression(); + } + + public async Task Reload() => await LazyLoader.Reload(); + + private Task LoadItems(LazyLoader _) + { + Items = Field.Invoke(RootItem).ToArray(); + + return Task.CompletedTask; + } + + private async Task StartUpdate(TItem item) + { + UpdateForm = Mapper.Map(item); + ItemToUpdate = item; + await UpdateModal.Show(); + } + + private async Task FinishUpdate() + { + var item = Mapper.Map(ItemToUpdate, UpdateForm!); + + if (ValidateUpdate != null) // Optional additional validation + await ValidateUpdate.Invoke(item); + + //ItemRepository.Update(item); + + // Reset + await UpdateModal.Hide(); + await LazyLoader.Reload(); + await ToastService.Success("Successfully updated item"); + } + + private async Task StartCreate() + { + CreateForm = Activator.CreateInstance()!; + await CreateModal.Show(); + } + + private async Task FinishCreate() + { + var item = Mapper.Map(CreateForm!); + + if (ValidateAdd != null) // Optional additional validation + await ValidateAdd.Invoke(item); + + Field.Invoke(RootItem).Add(item); + + // Reset + await CreateModal.Hide(); + await LazyLoader.Reload(); + await ToastService.Success("Successfully added item"); + } + + private async Task StartDelete(TItem item) + { + ItemToDelete = item; + await DeleteModal.Show(); + } + + private async Task FinishDelete() + { + if (ValidateDelete != null) // Optional additional validation + await ValidateDelete.Invoke(ItemToDelete); + + Field.Invoke(RootItem).Remove(ItemToDelete); + + // Reset + await DeleteModal.Hide(); + await LazyLoader.Reload(); + await ToastService.Success("Successfully deleted item"); + } + + private Expression> CreateExpression() + { + // Parameter expression for the input object + var inputParam = Expression.Parameter(typeof(TItem), "input"); + + // Convert the input object to the actual model type (MyModel in this example) + var castedInput = Expression.Convert(inputParam, typeof(TItem)); + + // Create a property access expression using the property name + var propertyAccess = Expression.Property(castedInput, "Id"); + + // Convert the property value to an object + var castedResult = Expression.Convert(propertyAccess, typeof(object)); + + // Create a lambda expression + var lambda = Expression.Lambda>(castedResult, inputParam); + + return lambda; + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Entities/ServerImage.cs b/Moonlight/Features/Servers/Entities/ServerImage.cs index 464df69..485df19 100644 --- a/Moonlight/Features/Servers/Entities/ServerImage.cs +++ b/Moonlight/Features/Servers/Entities/ServerImage.cs @@ -19,7 +19,7 @@ public class ServerImage public string? DonateUrl { get; set; } public string? UpdateUrl { get; set; } - public List Variables = new(); + public List Variables { get; set; } = new(); public int DefaultDockerImageIndex { get; set; } = 0; public List DockerImages { get; set; } diff --git a/Moonlight/Features/Servers/Models/Forms/Admin/Images/ParseConfigForm.cs b/Moonlight/Features/Servers/Models/Forms/Admin/Images/ParseConfigForm.cs new file mode 100644 index 0000000..ecff53e --- /dev/null +++ b/Moonlight/Features/Servers/Models/Forms/Admin/Images/ParseConfigForm.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Features.Servers.Models.Forms.Admin.Images; + +public class ParseConfigForm +{ + [Required(ErrorMessage = "You need to specify a type in a parse configuration")] + public string Type { get; set; } = ""; + + [Required(ErrorMessage = "You need to specify a file in a parse configuration")] + public string File { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Forms/Admin/Images/ParseConfigOptionForm.cs b/Moonlight/Features/Servers/Models/Forms/Admin/Images/ParseConfigOptionForm.cs new file mode 100644 index 0000000..0fdc0b4 --- /dev/null +++ b/Moonlight/Features/Servers/Models/Forms/Admin/Images/ParseConfigOptionForm.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Features.Servers.Models.Forms.Admin.Images; + +public class ParseConfigOptionForm +{ + [Required(ErrorMessage = "You need to specify the key of an option")] + public string Key { get; set; } = ""; + public string Value { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/ServerParseConfig.cs b/Moonlight/Features/Servers/Models/ServerParseConfig.cs new file mode 100644 index 0000000..417c809 --- /dev/null +++ b/Moonlight/Features/Servers/Models/ServerParseConfig.cs @@ -0,0 +1,8 @@ +namespace Moonlight.Features.Servers.Models; + +public class ServerParseConfig +{ + public string Type { get; set; } = ""; + public string File { get; set; } = ""; + public Dictionary Configuration { get; set; } = new(); +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Components/ImageEditor.razor b/Moonlight/Features/Servers/UI/Components/ImageEditor.razor new file mode 100644 index 0000000..5bddd99 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Components/ImageEditor.razor @@ -0,0 +1,154 @@ + +
+
+ General +
+
+
+
+ + +
+
+ + +
+
+ +
+ (Optional) URL to link donation pages for the author +
+ +
+
+ +
+ (Optional) URL to enable auto updates on images. This link needs to be a direct download link to a json file +
+ +
+
+
+
+
+
+ Installation +
+
+
+
+ +
+ This specifies the docker image to use for the script execution +
+ +
+
+ +
+ This is the shell to pass the install script to +
+ +
+
+ @* TODO: Add vscode editor or similar *@ + + +
+
+
+
+ Startup, Control & Allocations +
+
+ +
+ This command gets passed to the container of the image to execute. Server variables can be used here +
+ +
+
+ +
+ This command will get written into the input stream of the server process when the server should get stopped +
+ +
+
+ +
+ The regex string you specify here will be used in order to detect if a server is up and running +
+ +
+
+ +
+ The allocations (aka. ports) a image needs in order to be created +
+ +
+
+
+
+
+
+ Docker images +
+ +
+
+
+
+ @foreach (var dockerImage in DockerImages) + { +
+
+ + Remove +
+
+ } +
+
+
+
+
+ Variables +
+ +
+
+
+
+ @foreach (var variable in Variables) + { +
+
+ + + Remove +
+
+ } +
+
+
+
+ +
+
+
+ +
+
+
+ +@code +{ + +} diff --git a/Moonlight/Features/Servers/UI/Components/ParseConfigEditor.razor b/Moonlight/Features/Servers/UI/Components/ParseConfigEditor.razor new file mode 100644 index 0000000..2f5e40b --- /dev/null +++ b/Moonlight/Features/Servers/UI/Components/ParseConfigEditor.razor @@ -0,0 +1,156 @@ +@using Newtonsoft.Json +@using Mappy.Net +@using Moonlight.Core.Helpers +@using Moonlight.Features.Servers.Models +@using Moonlight.Features.Servers.Models.Forms.Admin.Images + +
+
+ Parse configurations +
+ +
+
+
+ @foreach (var config in Configs) + { +
+
+
+ +
+
+
+
+
+ +
+ A relative path from the servers main directory to the file you want to modify +
+ +
+
+ +
+ This specifies the type of parser to use. e.g. "properties" or "file" +
+ +
+
+
+ Remove +
+
+
+
+
+
+
+ +
+
+
+
+ @foreach (var option in config.Value) + { +
+
+ + + Remove +
+
+ } +
+
+
+
+
+
+
+ } +
+
+ +@code +{ + [Parameter] + public string InitialContent { get; set; } = ""; + + private Dictionary> Configs = new(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !string.IsNullOrEmpty(InitialContent)) + await Set(InitialContent); + } + + public async Task Set(string content) + { + Configs.Clear(); + + var configs = JsonConvert.DeserializeObject(content) + ?? Array.Empty(); + + foreach (var config in configs) + { + var options = config.Configuration.Select(x => new ParseConfigOptionForm() + { + Key = x.Key, + Value = x.Value + }).ToList(); + + + Configs.Add(Mapper.Map(config), options); + } + + await InvokeAsync(StateHasChanged); + } + + public async Task ValidateAndGet() + { + await ValidatorHelper.ValidateRange(Configs.Keys); + + foreach (var options in Configs.Values) + await ValidatorHelper.ValidateRange(options); + + var finalConfigs = Configs.Select(x => new ServerParseConfig() + { + File = x.Key.File, + Type = x.Key.Type, + Configuration = x.Value.ToDictionary(y => y.Key, y => y.Value) + }).ToList(); + + return JsonConvert.SerializeObject(finalConfigs); + } + + private async Task AddConfig() + { + Configs.Add(new(), new()); + await InvokeAsync(StateHasChanged); + } + + private async Task RemoveConfig(ParseConfigForm config) + { + Configs.Remove(config); + await InvokeAsync(StateHasChanged); + } + + private async Task AddOption(ParseConfigForm config) + { + Configs[config].Add(new()); + await InvokeAsync(StateHasChanged); + } + + private async Task RemoveOption(ParseConfigForm config, ParseConfigOptionForm option) + { + Configs[config].Remove(option); + await InvokeAsync(StateHasChanged); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor b/Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor new file mode 100644 index 0000000..eba6f84 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor @@ -0,0 +1,129 @@ +@page "/admin/servers/images" + +@using BlazorTable +@using Microsoft.EntityFrameworkCore +@using Moonlight.Core.Exceptions +@using Moonlight.Core.Repositories +@using Moonlight.Core.Services.Interop +@using Moonlight.Features.Servers.Entities + +@inject Repository ImageRepository +@inject Repository ImageVariableRepository +@inject Repository DockerImageRepository +@inject Repository ServerRepository +@inject ToastService ToastService + +
+
+

Manage server images

+
+ + + +
+
+
+ + + + + + + + + + + +
+
+
+
+ +@code +{ + private LazyLoader LazyLoader; + private ServerImage[] Images; + + private Task Load(LazyLoader arg) + { + Images = ImageRepository + .Get() + .ToArray(); + + return Task.CompletedTask; + } + + private async Task Update(ServerImage image) + { + } + + private async Task Delete(ServerImage image) + { + var anyServerWithThisImage = ServerRepository + .Get() + .Any(x => x.Image.Id == image.Id); + + if (anyServerWithThisImage) + throw new DisplayException("This image cannot be deleted, because one or more server depend on it. Delete the server(s) first and then delete the image"); + + var imageWithData = ImageRepository + .Get() + .Include(x => x.Variables) + .Include(x => x.DockerImages) + .First(x => x.Id == image.Id); + + // Cache and clear relational data + + var variables = imageWithData.Variables.ToArray(); + var dockerImages = imageWithData.DockerImages.ToArray(); + + imageWithData.DockerImages.Clear(); + imageWithData.Variables.Clear(); + + ImageRepository.Update(imageWithData); + + foreach (var variable in variables) + ImageVariableRepository.Delete(variable); + + foreach (var dockerImage in dockerImages) + DockerImageRepository.Delete(dockerImage); + + // And now we can clear the image + + ImageRepository.Delete(imageWithData); + + // and notify the user + await ToastService.Success("Successfully deleted image"); + await LazyLoader.Reload(); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Images/New.razor b/Moonlight/Features/Servers/UI/Views/Admin/Images/New.razor new file mode 100644 index 0000000..f170d79 --- /dev/null +++ b/Moonlight/Features/Servers/UI/Views/Admin/Images/New.razor @@ -0,0 +1,10 @@ +@page "/admin/servers/images/new" + +@using Moonlight.Features.Servers.Entities + + + +@code +{ + private ServerImage Image; +} From 4e5124cc1b12541502a0828976c680056e007f5c Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 28 Jan 2024 17:50:20 +0100 Subject: [PATCH 04/37] Added image update pages. Added forms models. Implemented parts of the image editor components --- .../UI/Components/Forms/AutoListCrud.razor | 13 +- .../Core/UI/Components/Partials/Sidebar.razor | 11 ++ .../Forms/Admin/Images/CreateDockerImage.cs | 18 ++ .../Forms/Admin/Images/CreateImageForm.cs | 19 +++ .../Images/{ => Parsing}/ParseConfigForm.cs | 2 +- .../{ => Parsing}/ParseConfigOptionForm.cs | 2 +- .../Forms/Admin/Images/UpdateDockerImage.cs | 18 ++ .../Admin/Images/UpdateImageDetailsForm.cs | 23 +++ .../Forms/Admin/Images/UpdateImageForm.cs | 19 +++ .../Images/UpdateImageInstallationForm.cs | 18 ++ .../Components/AdminImageViewNavigation.razor | 43 +++++ .../Servers/UI/Components/ImageEditor.razor | 154 ------------------ .../UI/Components/ParseConfigEditor.razor | 1 + .../Servers/UI/Views/Admin/Images/Index.razor | 13 +- .../Servers/UI/Views/Admin/Images/New.razor | 47 +++++- .../Admin/Images/View/ConfigParsing.razor | 35 ++++ .../UI/Views/Admin/Images/View/Details.razor | 70 ++++++++ .../Admin/Images/View/DockerImages.razor | 65 ++++++++ .../UI/Views/Admin/Images/View/Index.razor | 70 ++++++++ .../Admin/Images/View/Installation.razor | 70 ++++++++ .../Views/Admin/Images/View/Variables.razor | 35 ++++ 21 files changed, 585 insertions(+), 161 deletions(-) create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/Images/CreateDockerImage.cs create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/Images/CreateImageForm.cs rename Moonlight/Features/Servers/Models/Forms/Admin/Images/{ => Parsing}/ParseConfigForm.cs (82%) rename Moonlight/Features/Servers/Models/Forms/Admin/Images/{ => Parsing}/ParseConfigOptionForm.cs (76%) create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/Images/UpdateDockerImage.cs create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/Images/UpdateImageDetailsForm.cs create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/Images/UpdateImageForm.cs create mode 100644 Moonlight/Features/Servers/Models/Forms/Admin/Images/UpdateImageInstallationForm.cs create mode 100644 Moonlight/Features/Servers/UI/Components/AdminImageViewNavigation.razor delete mode 100644 Moonlight/Features/Servers/UI/Components/ImageEditor.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Images/View/ConfigParsing.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Images/View/Details.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Images/View/DockerImages.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Images/View/Index.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Images/View/Installation.razor create mode 100644 Moonlight/Features/Servers/UI/Views/Admin/Images/View/Variables.razor diff --git a/Moonlight/Core/UI/Components/Forms/AutoListCrud.razor b/Moonlight/Core/UI/Components/Forms/AutoListCrud.razor index 3945be9..6d1dc2d 100644 --- a/Moonlight/Core/UI/Components/Forms/AutoListCrud.razor +++ b/Moonlight/Core/UI/Components/Forms/AutoListCrud.razor @@ -1,6 +1,7 @@ @using BlazorTable @using System.Linq.Expressions @using Mappy.Net +@using Moonlight.Core.Repositories @using Moonlight.Core.Services.Interop @typeparam TItem where TItem : class @@ -9,6 +10,8 @@ @typeparam TUpdateForm @inject ToastService ToastService +@inject Repository RootRepository +@inject Repository ItemRepository
@@ -174,7 +177,7 @@ if (ValidateUpdate != null) // Optional additional validation await ValidateUpdate.Invoke(item); - //ItemRepository.Update(item); + ItemRepository.Update(item); // Reset await UpdateModal.Hide(); @@ -196,6 +199,7 @@ await ValidateAdd.Invoke(item); Field.Invoke(RootItem).Add(item); + RootRepository.Update(RootItem); // Reset await CreateModal.Hide(); @@ -215,6 +219,13 @@ await ValidateDelete.Invoke(ItemToDelete); Field.Invoke(RootItem).Remove(ItemToDelete); + RootRepository.Update(RootItem); + + try + { + ItemRepository.Delete(ItemToDelete); + } + catch (Exception) { /* ignored, as we dont want such an operation to fail the request */ } // Reset await DeleteModal.Hide(); diff --git a/Moonlight/Core/UI/Components/Partials/Sidebar.razor b/Moonlight/Core/UI/Components/Partials/Sidebar.razor index f03f3b4..61e6adf 100644 --- a/Moonlight/Core/UI/Components/Partials/Sidebar.razor +++ b/Moonlight/Core/UI/Components/Partials/Sidebar.razor @@ -94,6 +94,17 @@
+ +