diff --git a/Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.Designer.cs b/Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.Designer.cs
similarity index 99%
rename from Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.Designer.cs
rename to Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.Designer.cs
index c3e9640..bb8b080 100644
--- a/Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.Designer.cs
+++ b/Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.Designer.cs
@@ -8,7 +8,7 @@ using Moonlight.App.Database;
#nullable disable
-namespace Moonlight.App.DatabaseMigrations
+namespace Moonlight.App.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20230217152643_MigratedSomeModels")]
diff --git a/Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.cs b/Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.cs
similarity index 98%
rename from Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.cs
rename to Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.cs
index 4c17501..ba2588c 100644
--- a/Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.cs
+++ b/Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.cs
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
-namespace Moonlight.App.DatabaseMigrations
+namespace Moonlight.App.Database.Migrations
{
///
public partial class MigratedSomeModels : Migration
diff --git a/Moonlight/App/Exceptions/WingsException.cs b/Moonlight/App/Exceptions/WingsException.cs
new file mode 100644
index 0000000..ca9c4b4
--- /dev/null
+++ b/Moonlight/App/Exceptions/WingsException.cs
@@ -0,0 +1,32 @@
+using System.Runtime.Serialization;
+
+namespace Moonlight.App.Exceptions.Wings;
+
+[Serializable]
+public class WingsException : Exception
+{
+ public int StatusCode { private get; set; }
+
+ public WingsException()
+ {
+ }
+
+ public WingsException(string message, int statusCode) : base(message)
+ {
+ StatusCode = statusCode;
+ }
+
+ public WingsException(string message) : base(message)
+ {
+ }
+
+ public WingsException(string message, Exception inner) : base(message, inner)
+ {
+ }
+
+ protected WingsException(
+ SerializationInfo info,
+ StreamingContext context) : base(info, context)
+ {
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Helpers/MonacoTypeHelper.cs b/Moonlight/App/Helpers/MonacoTypeHelper.cs
new file mode 100644
index 0000000..8e37c19
--- /dev/null
+++ b/Moonlight/App/Helpers/MonacoTypeHelper.cs
@@ -0,0 +1,44 @@
+namespace Moonlight.App.Helpers;
+
+public class MonacoTypeHelper
+{
+ public static string GetEditorType(string file)
+ {
+ var extension = Path.GetExtension(file);
+ extension = extension.TrimStart("."[0]);
+
+ switch (extension)
+ {
+ case "bat":
+ return "bat";
+ case "cs":
+ return "csharp";
+ case "css":
+ return "css";
+ case "html":
+ return "html";
+ case "java":
+ return "java";
+ case "js":
+ return "javascript";
+ case "ini":
+ return "ini";
+ case "json":
+ return "json";
+ case "lua":
+ return "lua";
+ case "php":
+ return "php";
+ case "py":
+ return "python";
+ case "sh":
+ return "shell";
+ case "xml":
+ return "xml";
+ case "yml":
+ return "yaml";
+ default:
+ return "plaintext";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Helpers/PaperApiHelper.cs b/Moonlight/App/Helpers/PaperApiHelper.cs
new file mode 100644
index 0000000..129e545
--- /dev/null
+++ b/Moonlight/App/Helpers/PaperApiHelper.cs
@@ -0,0 +1,48 @@
+using Newtonsoft.Json;
+using RestSharp;
+
+namespace Moonlight.App.Helpers;
+
+public class PaperApiHelper
+{
+ private string ApiUrl { get; set; }
+
+ public PaperApiHelper()
+ {
+ ApiUrl = "https://api.papermc.io/v2/projects/";
+ }
+
+ public async Task Get(string url)
+ {
+ RestClient client = new();
+
+ string requrl = "NONSET";
+
+ if (ApiUrl.EndsWith("/"))
+ requrl = ApiUrl + url;
+ else
+ requrl = ApiUrl + "/" + url;
+
+ RestRequest request = new(requrl);
+
+ request.AddHeader("Content-Type", "application/json");
+
+ var response = await client.GetAsync(request);
+
+ if (!response.IsSuccessful)
+ {
+ if (response.StatusCode != 0)
+ {
+ throw new Exception(
+ $"An error occured: ({response.StatusCode}) {response.Content}"
+ );
+ }
+ else
+ {
+ throw new Exception($"An internal error occured: {response.ErrorMessage}");
+ }
+ }
+
+ return JsonConvert.DeserializeObject(response.Content);
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Helpers/ParseHelper.cs b/Moonlight/App/Helpers/ParseHelper.cs
new file mode 100644
index 0000000..ecc983d
--- /dev/null
+++ b/Moonlight/App/Helpers/ParseHelper.cs
@@ -0,0 +1,32 @@
+namespace Moonlight.App.Helpers;
+
+public static class ParseHelper
+{
+ public static int MinecraftToInt(string raw)
+ {
+ var versionWithoutPre = raw.Split("-")[0];
+
+ if (versionWithoutPre.Count(x => x == "."[0]) == 1)
+ versionWithoutPre += ".0";
+
+ return int.Parse(versionWithoutPre.Replace(".", ""));
+ }
+
+ public static string FirstPartStartingWithNumber(string raw)
+ {
+ var numbers = "0123456789";
+ var res = "";
+ var found = false;
+
+ foreach (var p in raw)
+ {
+ if (!found)
+ found = numbers.Contains(p);
+
+ if (found)
+ res += p;
+ }
+
+ return res;
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Helpers/StreamProgressHelper.cs b/Moonlight/App/Helpers/StreamProgressHelper.cs
new file mode 100644
index 0000000..ae669f4
--- /dev/null
+++ b/Moonlight/App/Helpers/StreamProgressHelper.cs
@@ -0,0 +1,40 @@
+namespace Moonlight.App.Helpers;
+
+public class StreamProgressHelper : Stream
+{
+ public Action? Progress { get; set; }
+ private int lastPercent = -1;
+
+ Stream InnerStream { get; init; }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ var result = InnerStream.ReadAsync(buffer, offset, count).Result;
+
+ int percentComplete = (int)Math.Round((double)(100 * Position) / Length);
+
+ if (lastPercent != percentComplete)
+ {
+ Progress?.Invoke(percentComplete);
+ lastPercent = percentComplete;
+ }
+
+ return result;
+ }
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ InnerStream.WriteAsync(buffer, offset, count);
+ }
+ public override bool CanRead => InnerStream.CanRead;
+ public override bool CanSeek => InnerStream.CanSeek;
+ public override bool CanWrite => InnerStream.CanWrite;
+ public override long Length => InnerStream.Length;
+ public override long Position { get => InnerStream.Position; set => InnerStream.Position = value; }
+ public StreamProgressHelper(Stream s)
+ {
+ this.InnerStream = s;
+ }
+ public override void Flush() => InnerStream.Flush();
+ public override long Seek(long offset, SeekOrigin origin) => InnerStream.Seek(offset, origin);
+ public override void SetLength(long value)=> InnerStream.SetLength(value);
+}
\ No newline at end of file
diff --git a/Moonlight/App/Helpers/WingsApiHelper.cs b/Moonlight/App/Helpers/WingsApiHelper.cs
new file mode 100644
index 0000000..76ad741
--- /dev/null
+++ b/Moonlight/App/Helpers/WingsApiHelper.cs
@@ -0,0 +1,228 @@
+using Moonlight.App.Database.Entities;
+using Moonlight.App.Exceptions.Wings;
+using Newtonsoft.Json;
+using RestSharp;
+
+namespace Moonlight.App.Helpers;
+
+public class WingsApiHelper
+{
+ private readonly RestClient Client;
+
+ public WingsApiHelper()
+ {
+ Client = new();
+ }
+
+ private string GetApiUrl(Node node)
+ {
+ if(node.Ssl)
+ return $"https://{node.Fqdn}:{node.HttpPort}/";
+ else
+ return $"http://{node.Fqdn}:{node.HttpPort}/";
+ //return $"https://{node.Fqdn}:{node.HttpPort}/";
+ }
+
+ public async Task Get(Node node, string resource)
+ {
+ RestRequest request = new(GetApiUrl(node) + resource);
+
+ request.AddHeader("Content-Type", "application/json");
+ request.AddHeader("Accept", "application/json");
+ request.AddHeader("Authorization", "Bearer " + node.Token);
+
+ var response = await Client.GetAsync(request);
+
+ if (!response.IsSuccessful)
+ {
+ if (response.StatusCode != 0)
+ {
+ throw new WingsException(
+ $"An error occured: ({response.StatusCode}) {response.Content}",
+ (int)response.StatusCode
+ );
+ }
+ else
+ {
+ throw new Exception($"An internal error occured: {response.ErrorMessage}");
+ }
+ }
+
+ return JsonConvert.DeserializeObject(response.Content!)!;
+ }
+
+ public async Task GetRaw(Node node, string resource)
+ {
+ RestRequest request = new(GetApiUrl(node) + resource);
+
+ request.AddHeader("Content-Type", "application/json");
+ request.AddHeader("Accept", "application/json");
+ request.AddHeader("Authorization", "Bearer " + node.Token);
+
+ var response = await Client.GetAsync(request);
+
+ if (!response.IsSuccessful)
+ {
+ if (response.StatusCode != 0)
+ {
+ throw new WingsException(
+ $"An error occured: ({response.StatusCode}) {response.Content}",
+ (int)response.StatusCode
+ );
+ }
+ else
+ {
+ throw new Exception($"An internal error occured: {response.ErrorMessage}");
+ }
+ }
+
+ return response.Content!;
+ }
+
+ public async Task Post(Node node, string resource, object? body)
+ {
+ RestRequest request = new(GetApiUrl(node) + resource);
+
+ request.AddHeader("Content-Type", "application/json");
+ request.AddHeader("Accept", "application/json");
+ request.AddHeader("Authorization", "Bearer " + node.Token);
+
+ request.AddParameter("text/plain",
+ JsonConvert.SerializeObject(body),
+ ParameterType.RequestBody
+ );
+
+ var response = await Client.PostAsync(request);
+
+ if (!response.IsSuccessful)
+ {
+ if (response.StatusCode != 0)
+ {
+ throw new WingsException(
+ $"An error occured: ({response.StatusCode}) {response.Content}",
+ (int)response.StatusCode
+ );
+ }
+ else
+ {
+ throw new Exception($"An internal error occured: {response.ErrorMessage}");
+ }
+ }
+
+ return JsonConvert.DeserializeObject(response.Content!)!;
+ }
+
+ public async Task Post(Node node, string resource, object? body)
+ {
+ RestRequest request = new(GetApiUrl(node) + resource);
+
+ request.AddHeader("Content-Type", "application/json");
+ request.AddHeader("Accept", "application/json");
+ request.AddHeader("Authorization", "Bearer " + node.Token);
+
+ if(body != null)
+ request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
+
+ var response = await Client.PostAsync(request);
+
+ if (!response.IsSuccessful)
+ {
+ if (response.StatusCode != 0)
+ {
+ throw new WingsException(
+ $"An error occured: ({response.StatusCode}) {response.Content}",
+ (int)response.StatusCode
+ );
+ }
+ else
+ {
+ throw new Exception($"An internal error occured: {response.ErrorMessage}");
+ }
+ }
+ }
+
+ public async Task PostRaw(Node node, string resource, object body)
+ {
+ RestRequest request = new(GetApiUrl(node) + resource);
+
+ request.AddHeader("Content-Type", "application/json");
+ request.AddHeader("Accept", "application/json");
+ request.AddHeader("Authorization", "Bearer " + node.Token);
+
+ request.AddParameter("text/plain", body, ParameterType.RequestBody);
+
+ var response = await Client.PostAsync(request);
+
+ if (!response.IsSuccessful)
+ {
+ if (response.StatusCode != 0)
+ {
+ throw new WingsException(
+ $"An error occured: ({response.StatusCode}) {response.Content}",
+ (int)response.StatusCode
+ );
+ }
+ else
+ {
+ throw new Exception($"An internal error occured: {response.ErrorMessage}");
+ }
+ }
+ }
+
+ public async Task Delete(Node node, string resource, object? body)
+ {
+ RestRequest request = new(GetApiUrl(node) + resource);
+
+ request.AddHeader("Content-Type", "application/json");
+ request.AddHeader("Accept", "application/json");
+ request.AddHeader("Authorization", "Bearer " + node.Token);
+
+ if(body != null)
+ request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
+
+ var response = await Client.DeleteAsync(request);
+
+ if (!response.IsSuccessful)
+ {
+ if (response.StatusCode != 0)
+ {
+ throw new WingsException(
+ $"An error occured: ({response.StatusCode}) {response.Content}",
+ (int)response.StatusCode
+ );
+ }
+ else
+ {
+ throw new Exception($"An internal error occured: {response.ErrorMessage}");
+ }
+ }
+ }
+
+ public async Task Put(Node node, string resource, object? body)
+ {
+ RestRequest request = new(GetApiUrl(node) + resource);
+
+ request.AddHeader("Content-Type", "application/json");
+ request.AddHeader("Accept", "application/json");
+ request.AddHeader("Authorization", "Bearer " + node.Token);
+
+ request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
+
+ var response = await Client.PutAsync(request);
+
+ if (!response.IsSuccessful)
+ {
+ if (response.StatusCode != 0)
+ {
+ throw new WingsException(
+ $"An error occured: ({response.StatusCode}) {response.Content}",
+ (int)response.StatusCode
+ );
+ }
+ else
+ {
+ throw new Exception($"An internal error occured: {response.ErrorMessage}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Helpers/WingsConsoleHelper.cs b/Moonlight/App/Helpers/WingsConsoleHelper.cs
new file mode 100644
index 0000000..720a293
--- /dev/null
+++ b/Moonlight/App/Helpers/WingsConsoleHelper.cs
@@ -0,0 +1,104 @@
+using System.Security.Cryptography;
+using System.Text;
+using JWT.Algorithms;
+using JWT.Builder;
+using Microsoft.EntityFrameworkCore;
+using Moonlight.App.Database.Entities;
+using Moonlight.App.Repositories.Servers;
+using Moonlight.App.Services;
+
+namespace Moonlight.App.Helpers;
+
+public class WingsConsoleHelper
+{
+ private readonly ServerRepository ServerRepository;
+ private readonly WingsJwtHelper WingsJwtHelper;
+ private readonly string AppUrl;
+
+ public WingsConsoleHelper(
+ ServerRepository serverRepository,
+ ConfigService configService,
+ WingsJwtHelper wingsJwtHelper)
+ {
+ ServerRepository = serverRepository;
+ WingsJwtHelper = wingsJwtHelper;
+
+ AppUrl = configService.GetSection("Moonlight").GetValue("AppUrl");
+ }
+
+ public async Task ConnectWings(PteroConsole.NET.PteroConsole pteroConsole, Server server)
+ {
+ var serverData = ServerRepository
+ .Get()
+ .Include(x => x.Node)
+ .First(x => x.Id == server.Id);
+
+ var token = GenerateToken(serverData);
+
+ if (serverData.Node.Ssl)
+ {
+ await pteroConsole.Connect(
+ AppUrl,
+ $"wss://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws",
+ token
+ );
+ }
+ else
+ {
+ await pteroConsole.Connect(
+ AppUrl,
+ $"ws://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws",
+ token
+ );
+ }
+ }
+
+ public string GenerateToken(Server server)
+ {
+ var serverData = ServerRepository
+ .Get()
+ .Include(x => x.Node)
+ .First(x => x.Id == server.Id);
+
+ var userid = 1;
+ var secret = serverData.Node.Token;
+
+
+ using (MD5 md5 = MD5.Create())
+ {
+ var inputBytes = Encoding.ASCII.GetBytes(userid + serverData.Uuid.ToString());
+ var outputBytes = md5.ComputeHash(inputBytes);
+
+ var identifier = Convert.ToHexString(outputBytes).ToLower();
+ var weirdId = StringHelper.GenerateString(16);
+
+ var token = JwtBuilder.Create()
+ .AddHeader("jti", identifier)
+ .WithAlgorithm(new HMACSHA256Algorithm())
+ .WithSecret(secret)
+ .AddClaim("user_id", userid)
+ .AddClaim("server_uuid", serverData.Uuid.ToString())
+ .AddClaim("permissions", new[]
+ {
+ "*",
+ "admin.websocket.errors",
+ "admin.websocket.install",
+ "admin.websocket.transfer"
+ })
+ .AddClaim("jti", identifier)
+ .AddClaim("unique_id", weirdId)
+ .AddClaim("iat", DateTimeOffset.Now.ToUnixTimeSeconds())
+ .AddClaim("nbf", DateTimeOffset.Now.AddSeconds(-10).ToUnixTimeSeconds())
+ .AddClaim("exp", DateTimeOffset.Now.AddMinutes(10).ToUnixTimeSeconds())
+ .AddClaim("iss", AppUrl)
+ .AddClaim("aud", new[]
+ {
+ serverData.Node.Ssl ? $"https://{serverData.Node.Fqdn}" : $"http://{serverData.Node.Fqdn}"
+ })
+ .MustVerifySignature()
+ .Encode();
+
+ return token;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Helpers/WingsJwtHelper.cs b/Moonlight/App/Helpers/WingsJwtHelper.cs
new file mode 100644
index 0000000..221abd1
--- /dev/null
+++ b/Moonlight/App/Helpers/WingsJwtHelper.cs
@@ -0,0 +1,56 @@
+using System.Security.Cryptography;
+using System.Text;
+using JWT.Algorithms;
+using JWT.Builder;
+using Moonlight.App.Services;
+
+namespace Moonlight.App.Helpers;
+
+public class WingsJwtHelper
+{
+ private readonly ConfigService ConfigService;
+ private readonly string AppUrl;
+
+ public WingsJwtHelper(ConfigService configService)
+ {
+ ConfigService = configService;
+
+ AppUrl = ConfigService.GetSection("Moonlight").GetValue("AppUrl");
+ }
+
+ public string Generate(string secret, Action> claimsAction)
+ {
+ var userid = 1;
+
+ using MD5 md5 = MD5.Create();
+ var inputBytes = Encoding.ASCII.GetBytes(userid + Guid.NewGuid().ToString());
+ var outputBytes = md5.ComputeHash(inputBytes);
+
+ var identifier = Convert.ToHexString(outputBytes).ToLower();
+ var weirdId = StringHelper.GenerateString(16);
+
+ var builder = JwtBuilder.Create()
+ .AddHeader("jti", identifier)
+ .WithAlgorithm(new HMACSHA256Algorithm())
+ .WithSecret(secret)
+ .AddClaim("user_id", userid)
+ .AddClaim("jti", identifier)
+ .AddClaim("unique_id", weirdId)
+ .AddClaim("iat", DateTimeOffset.Now.ToUnixTimeSeconds())
+ .AddClaim("nbf", DateTimeOffset.Now.AddSeconds(-10).ToUnixTimeSeconds())
+ .AddClaim("exp", DateTimeOffset.Now.AddMinutes(10).ToUnixTimeSeconds())
+ .AddClaim("iss", AppUrl)
+ .MustVerifySignature();
+
+ var additionalClaims = new Dictionary();
+
+ claimsAction.Invoke(additionalClaims);
+
+ foreach (var claim in additionalClaims)
+ {
+ builder = builder.AddClaim(claim.Key, claim.Value);
+ }
+
+ return builder.Encode();
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Helpers/WingsServerConverter.cs b/Moonlight/App/Helpers/WingsServerConverter.cs
new file mode 100644
index 0000000..2b4c33d
--- /dev/null
+++ b/Moonlight/App/Helpers/WingsServerConverter.cs
@@ -0,0 +1,131 @@
+using System.Text;
+using Microsoft.EntityFrameworkCore;
+using Moonlight.App.Database.Entities;
+using Moonlight.App.Http.Resources.Wings;
+using Moonlight.App.Repositories;
+using Moonlight.App.Repositories.Servers;
+
+namespace Moonlight.App.Helpers;
+
+public class WingsServerConverter
+{
+ private readonly ServerRepository ServerRepository;
+ private readonly ImageRepository ImageRepository;
+
+ public WingsServerConverter(ServerRepository serverRepository, ImageRepository imageRepository)
+ {
+ ServerRepository = serverRepository;
+ ImageRepository = imageRepository;
+ }
+
+ public WingsServer FromServer(Server s)
+ {
+ var server = ServerRepository
+ .Get()
+ .Include(x => x.Allocations)
+ .Include(x => x.Backups)
+ .Include(x => x.Variables)
+ .Include(x => x.Image)
+ .Include(x => x.MainAllocation)
+ .First(x => x.Id == s.Id);
+
+ var wingsServer = new WingsServer
+ {
+ Uuid = server.Uuid
+ };
+
+ // Allocations
+ var def = server.MainAllocation;
+
+ wingsServer.Settings.Allocations.Default.Ip = "0.0.0.0";
+ wingsServer.Settings.Allocations.Default.Port = def.Port;
+
+ foreach (var a in server.Allocations)
+ {
+ wingsServer.Settings.Allocations.Mappings.Ports.Add(a.Port);
+ }
+
+ // Build
+ wingsServer.Settings.Build.Swap = server.Memory * 2;
+ wingsServer.Settings.Build.Threads = null!;
+ wingsServer.Settings.Build.Cpu_Limit = server.Cpu;
+ wingsServer.Settings.Build.Disk_Space = server.Disk;
+ wingsServer.Settings.Build.Io_Weight = 500;
+ wingsServer.Settings.Build.Memory_Limit = server.Memory;
+ wingsServer.Settings.Build.Oom_Disabled = true;
+
+ var image = ImageRepository
+ .Get()
+ .Include(x => x.DockerImages)
+ .First(x => x.Id == server.Image.Id);
+
+ // Container
+ wingsServer.Settings.Container.Image = image.DockerImages[server.DockerImageIndex].Name;
+
+ // Egg
+ wingsServer.Settings.Egg.Id = image.Uuid;
+
+ // Settings
+ wingsServer.Settings.Skip_Egg_Scripts = false;
+ wingsServer.Settings.Suspended = false; //TODO: Implement
+ wingsServer.Settings.Invocation = string.IsNullOrEmpty(server.OverrideStartup) ? image.Startup : server.OverrideStartup;
+ wingsServer.Settings.Uuid = server.Uuid;
+
+
+ // Environment
+ foreach (var v in server.Variables)
+ {
+ if (!wingsServer.Settings.Environment.ContainsKey(v.Key))
+ {
+ wingsServer.Settings.Environment.Add(v.Key, v.Value);
+ }
+ }
+
+ // Stop
+ if (image.StopCommand.StartsWith("!"))
+ {
+ wingsServer.Process_Configuration.Stop.Type = "stop";
+ wingsServer.Process_Configuration.Stop.Value = null!;
+ }
+ else
+ {
+ wingsServer.Process_Configuration.Stop.Type = "command";
+ wingsServer.Process_Configuration.Stop.Value = image.StopCommand;
+ }
+
+ // Done
+
+ wingsServer.Process_Configuration.Startup.Done = new() { image.StartupDetection };
+ wingsServer.Process_Configuration.Startup.Strip_Ansi = false;
+ wingsServer.Process_Configuration.Startup.User_Interaction = new();
+
+ // Configs
+ var configData = new ConfigurationBuilder().AddJsonStream(
+ new MemoryStream(Encoding.ASCII.GetBytes(image.ConfigFiles!))
+ ).Build();
+
+ foreach (var child in configData.GetChildren())
+ {
+ List replaces = new();
+
+ foreach (var section in child.GetSection("find").GetChildren())
+ {
+ replaces.Add(new()
+ {
+ Match = section.Key,
+ Replace_With = section.Value
+ .Replace("{{server.build.default.port}}", def.Port.ToString())
+ });
+ }
+
+ wingsServer.Process_Configuration.Configs.Add(new()
+ {
+ Parser = child.GetValue("parser"),
+ File = child.Key,
+ Replace = replaces
+ });
+ }
+
+ return wingsServer;
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Http/Controllers/Api/Remote/ActivityController.cs b/Moonlight/App/Http/Controllers/Api/Remote/ActivityController.cs
new file mode 100644
index 0000000..e1be3ce
--- /dev/null
+++ b/Moonlight/App/Http/Controllers/Api/Remote/ActivityController.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Moonlight.App.Http.Controllers.Api.Remote;
+
+[Route("api/remote/activity")]
+[ApiController]
+public class ActivityController : Controller
+{
+ [HttpPost]
+ public ActionResult SaveActivity()
+ {
+ return Ok();
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs b/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs
new file mode 100644
index 0000000..9ea1e8f
--- /dev/null
+++ b/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs
@@ -0,0 +1,202 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Moonlight.App.Helpers;
+using Moonlight.App.Http.Resources.Wings;
+using Moonlight.App.Repositories;
+using Moonlight.App.Repositories.Servers;
+using Moonlight.App.Services;
+
+namespace Moonlight.App.Http.Controllers.Api.Remote;
+
+[Route("api/remote/servers")]
+[ApiController]
+public class ServersController : Controller
+{
+ private readonly WingsServerConverter Converter;
+ private readonly ServerRepository ServerRepository;
+ private readonly NodeRepository NodeRepository;
+ private readonly MessageService MessageService;
+
+ public ServersController(
+ WingsServerConverter converter,
+ ServerRepository serverRepository,
+ NodeRepository nodeRepository,
+ MessageService messageService)
+ {
+ Converter = converter;
+ ServerRepository = serverRepository;
+ NodeRepository = nodeRepository;
+ MessageService = messageService;
+ }
+
+ [HttpGet]
+ public async Task>> GetServers(
+ [FromQuery(Name = "page")] int page,
+ [FromQuery(Name = "per_page")] int perPage)
+ {
+ var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
+ var id = tokenData.Split(".")[0];
+ var token = tokenData.Split(".")[1];
+
+ var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
+
+ if (node == null)
+ return NotFound();
+
+ if (token != node.Token)
+ return Unauthorized();
+
+ var servers = ServerRepository
+ .Get()
+ .Include(x => x.Node)
+ .Where(x => x.Node.Id == node.Id)
+ .ToArray();
+
+ List wingsServers = new();
+ int totalPages = 1;
+
+ if (servers.Length > 0)
+ {
+ var slice = servers.Chunk(perPage).ToArray();
+ var part = slice[page];
+
+ foreach (var server in part)
+ {
+ wingsServers.Add(Converter.FromServer(server));
+ }
+
+ totalPages = slice.Length - 1;
+ }
+
+ await MessageService.Emit($"wings.{node.Id}.serverlist", node);
+
+ //Logger.Debug($"[BRIDGE] Node '{node.Name}' is requesting server list page {page} with {perPage} items per page");
+
+ return PaginationResult.CreatePagination(
+ wingsServers.ToArray(),
+ page,
+ perPage,
+ totalPages,
+ servers.Length
+ );
+ }
+
+
+ [HttpPost("reset")]
+ public async Task Reset()
+ {
+ var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
+ var id = tokenData.Split(".")[0];
+ var token = tokenData.Split(".")[1];
+
+ var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
+
+ if (node == null)
+ return NotFound();
+
+ if (token != node.Token)
+ return Unauthorized();
+
+ await MessageService.Emit($"wings.{node.Id}.statereset", node);
+
+ foreach (var server in ServerRepository
+ .Get()
+ .Include(x => x.Node)
+ .Where(x => x.Node.Id == node.Id)
+ .ToArray()
+ )
+ {
+ if (server.Installing)
+ {
+ server.Installing = false;
+ ServerRepository.Update(server);
+ }
+ }
+
+ return Ok();
+ }
+
+ [HttpGet("{uuid}")]
+ public async Task> GetServer(Guid uuid)
+ {
+ var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
+ var id = tokenData.Split(".")[0];
+ var token = tokenData.Split(".")[1];
+
+ var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
+
+ if (node == null)
+ return NotFound();
+
+ if (token != node.Token)
+ return Unauthorized();
+
+ var server = ServerRepository.Get().FirstOrDefault(x => x.Uuid == uuid);
+
+ if (server == null)
+ return NotFound();
+
+ await MessageService.Emit($"wings.{node.Id}.serverfetch", server);
+
+ return Converter.FromServer(server);
+ }
+
+ [HttpGet("{uuid}/install")]
+ public async Task> GetServerInstall(Guid uuid)
+ {
+ var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
+ var id = tokenData.Split(".")[0];
+ var token = tokenData.Split(".")[1];
+
+ var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
+
+ if (node == null)
+ return NotFound();
+
+ if (token != node.Token)
+ return Unauthorized();
+
+ var server = ServerRepository.Get().Include(x => x.Image).FirstOrDefault(x => x.Uuid == uuid);
+
+ if (server == null)
+ return NotFound();
+
+ await MessageService.Emit($"wings.{node.Id}.serverinstallfetch", server);
+
+ return new WingsServerInstall()
+ {
+ Entrypoint = server.Image.InstallEntrypoint,
+ Script = server.Image.InstallScript!,
+ Container_Image = server.Image.InstallDockerImage
+ };
+ }
+
+ [HttpPost("{uuid}/install")]
+ public async Task SetInstallState(Guid uuid)
+ {
+ var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
+ var id = tokenData.Split(".")[0];
+ var token = tokenData.Split(".")[1];
+
+ var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
+
+ if (node == null)
+ return NotFound();
+
+ if (token != node.Token)
+ return Unauthorized();
+
+ var server = ServerRepository.Get().Include(x => x.Image).FirstOrDefault(x => x.Uuid == uuid);
+
+ if (server == null)
+ return NotFound();
+
+ server.Installing = false;
+ ServerRepository.Update(server);
+
+ await MessageService.Emit($"wings.{node.Id}.serverinstallcomplete", server);
+ await MessageService.Emit($"server.{server.Uuid}.installcomplete", server);
+
+ return Ok();
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Http/Requests/Wings/ReportBackupCompleteRequest.cs b/Moonlight/App/Http/Requests/Wings/ReportBackupCompleteRequest.cs
new file mode 100644
index 0000000..cacacec
--- /dev/null
+++ b/Moonlight/App/Http/Requests/Wings/ReportBackupCompleteRequest.cs
@@ -0,0 +1,7 @@
+namespace Moonlight.App.Http.Requests.Wings;
+
+public class ReportBackupCompleteRequest
+{
+ public bool Successful { get; set; }
+ public long Size { get; set; }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Http/Requests/Wings/SftpLoginRequest.cs b/Moonlight/App/Http/Requests/Wings/SftpLoginRequest.cs
new file mode 100644
index 0000000..4057e98
--- /dev/null
+++ b/Moonlight/App/Http/Requests/Wings/SftpLoginRequest.cs
@@ -0,0 +1,9 @@
+namespace Moonlight.App.Http.Requests.Wings;
+
+public class SftpLoginRequest
+{
+ public string Username { get; set; }
+ public string Password { get; set; }
+ public string Ip { get; set; }
+ public string Type { get; set; }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Http/Resources/Wings/PaginationResult.cs b/Moonlight/App/Http/Resources/Wings/PaginationResult.cs
new file mode 100644
index 0000000..610e178
--- /dev/null
+++ b/Moonlight/App/Http/Resources/Wings/PaginationResult.cs
@@ -0,0 +1,47 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Http.Resources.Wings;
+
+public class PaginationResult
+{
+ [JsonProperty("data")]
+ public List Data { get; set; }
+
+ [JsonProperty("meta")]
+ public MetaData Meta { get; set; }
+
+ public PaginationResult()
+ {
+ Data = new List();
+ Meta = new();
+ }
+
+ public static PaginationResult CreatePagination(T[] data, int page, int perPage, int totalPages, int totalItems)
+ {
+ var res = new PaginationResult();
+
+ foreach (var i in data)
+ {
+ res.Data.Add(i);
+ }
+
+ res.Meta.Current_Page = page;
+ res.Meta.Total_Pages = totalPages;
+ res.Meta.Count = data.Length;
+ res.Meta.Total = totalItems;
+ res.Meta.Per_Page = perPage;
+ res.Meta.Last_Page = totalPages;
+
+ return res;
+ }
+
+ public class MetaData
+ {
+ public int Total { get; set; }
+ public int Count { get; set; }
+ public int Per_Page { get; set; }
+ public int Current_Page { get; set; }
+ public int Last_Page { get; set; }
+ public int Total_Pages { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Http/Resources/Wings/SftpLoginResult.cs b/Moonlight/App/Http/Resources/Wings/SftpLoginResult.cs
new file mode 100644
index 0000000..54d0ea6
--- /dev/null
+++ b/Moonlight/App/Http/Resources/Wings/SftpLoginResult.cs
@@ -0,0 +1,8 @@
+namespace Moonlight.App.Http.Resources.Wings;
+
+public class SftpLoginResult
+{
+ public string Server { get; set; }
+ public string User { get; set; }
+ public List Permissions { get; set; }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Http/Resources/Wings/WingsServer.cs b/Moonlight/App/Http/Resources/Wings/WingsServer.cs
new file mode 100644
index 0000000..d55a81e
--- /dev/null
+++ b/Moonlight/App/Http/Resources/Wings/WingsServer.cs
@@ -0,0 +1,154 @@
+using System.Text.Json.Serialization;
+
+namespace Moonlight.App.Http.Resources.Wings;
+
+public class WingsServer
+{
+ [JsonPropertyName("uuid")]
+ public Guid Uuid { get; set; }
+
+ [JsonPropertyName("settings")] public WingsServerSettings Settings { get; set; } = new();
+
+ [JsonPropertyName("process_configuration")]
+ public WingsServerProcessConfiguration Process_Configuration { get; set; } = new();
+
+ public class WingsServerProcessConfiguration
+ {
+ [JsonPropertyName("startup")] public WingsServerStartup Startup { get; set; } = new();
+
+ [JsonPropertyName("stop")] public WingsServerStop Stop { get; set; } = new();
+
+ [JsonPropertyName("configs")] public List Configs { get; set; } = new();
+ }
+
+ public class WingsServerConfig
+ {
+ [JsonPropertyName("parser")]
+ public string Parser { get; set; }
+
+ [JsonPropertyName("file")]
+ public string File { get; set; }
+
+ [JsonPropertyName("replace")] public List Replace { get; set; } = new();
+ }
+
+ public class WingsServerReplace
+ {
+ [JsonPropertyName("match")]
+ public string Match { get; set; }
+
+ [JsonPropertyName("replace_with")]
+ public string Replace_With { get; set; }
+ }
+
+ public class WingsServerStartup
+ {
+ [JsonPropertyName("done")] public List Done { get; set; } = new();
+
+ [JsonPropertyName("user_interaction")] public List