diff --git a/Moonlight/App/Events/EventSystem.cs b/Moonlight/App/Events/EventSystem.cs index 1d0f1d40..58b27ded 100644 --- a/Moonlight/App/Events/EventSystem.cs +++ b/Moonlight/App/Events/EventSystem.cs @@ -5,7 +5,6 @@ namespace Moonlight.App.Events; public class EventSystem { - private Dictionary Storage = new(); private List Subscribers = new(); private readonly bool Debug = false; @@ -33,16 +32,8 @@ public class EventSystem return Task.CompletedTask; } - public Task Emit(string id, object? d = null) + public Task Emit(string id, object? data = null) { - int hashCode = -1; - - if (d != null) - { - hashCode = d.GetHashCode(); - Storage.TryAdd(hashCode, d); - } - Subscriber[] subscribers; lock (Subscribers) @@ -58,23 +49,6 @@ public class EventSystem { tasks.Add(new Task(() => { - int storageId = hashCode + 0; // To create a copy of the hash code - - object? data = null; - - if (storageId != -1) - { - if (Storage.TryGetValue(storageId, out var value)) - { - data = value; - } - else - { - Logger.Warn($"Object with the hash '{storageId}' was not present in the storage"); - return; - } - } - var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -115,8 +89,7 @@ public class EventSystem Task.Run(() => { Task.WaitAll(tasks.ToArray()); - Storage.Remove(hashCode); - + if(Debug) Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage"); }); diff --git a/Moonlight/App/Helpers/Files/WingsFileAccess.cs b/Moonlight/App/Helpers/Files/WingsFileAccess.cs index 47acdc9d..f28d65a2 100644 --- a/Moonlight/App/Helpers/Files/WingsFileAccess.cs +++ b/Moonlight/App/Helpers/Files/WingsFileAccess.cs @@ -3,6 +3,7 @@ using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings.Requests; using Moonlight.App.ApiClients.Wings.Resources; using Moonlight.App.Database.Entities; +using Moonlight.App.Helpers.Wings; using Moonlight.App.Services; using RestSharp; @@ -211,13 +212,27 @@ public class WingsFileAccess : FileAccess public override async Task Decompress(FileData fileData) { - var req = new DecompressFile() + try { - Root = CurrentPath, - File = fileData.Name - }; + var req = new DecompressFile() + { + Root = CurrentPath, + File = fileData.Name + }; - await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req); + await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req); + } + catch (Exception e) + { + if (e.Message.ToLower().Contains("canceled")) + { + // ignore, maybe do smth better here, like showing a waiting thing or so + } + else + { + throw; + } + } } public override Task GetLaunchUrl() diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index 78fbfc55..99a8ab62 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -8,7 +8,14 @@ public static class Formatter { TimeSpan t = TimeSpan.FromMilliseconds(uptime); - return $"{t.Hours}h {t.Minutes}m {t.Seconds}s"; + if (t.Days > 0) + { + return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s"; + } + else + { + return $"{t.Hours}h {t.Minutes}m {t.Seconds}s"; + } } private static double Round(this double d, int decimals) diff --git a/Moonlight/App/Helpers/Wings/Data/ConsoleMessage.cs b/Moonlight/App/Helpers/Wings/Data/ConsoleMessage.cs new file mode 100644 index 00000000..87d86f6d --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Data/ConsoleMessage.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Helpers.Wings.Data; + +public class ConsoleMessage +{ + public string Content { get; set; } = ""; + public bool IsInternal { get; set; } = false; +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Data/ServerResource.cs b/Moonlight/App/Helpers/Wings/Data/ServerResource.cs new file mode 100644 index 00000000..2b7ec53f --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Data/ServerResource.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Helpers.Wings.Data; + +public class ServerResource +{ + [JsonProperty("memory_bytes")] + public long MemoryBytes { get; set; } + + [JsonProperty("memory_limit_bytes")] + public long MemoryLimitBytes { get; set; } + + [JsonProperty("cpu_absolute")] + public float CpuAbsolute { get; set; } + + [JsonProperty("network")] + public NetworkData Network { get; set; } + + [JsonProperty("uptime")] + public double Uptime { get; set; } + + [JsonProperty("state")] + public string State { get; set; } + + [JsonProperty("disk_bytes")] + public long DiskBytes { get; set; } + + public class NetworkData + { + [JsonProperty("rx_bytes")] + public long RxBytes { get; set; } + + [JsonProperty("tx_bytes")] + public long TxBytes { get; set; } + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Enums/ConsoleState.cs b/Moonlight/App/Helpers/Wings/Enums/ConsoleState.cs new file mode 100644 index 00000000..b2fce78e --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Enums/ConsoleState.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Helpers.Wings.Enums; + +public enum ConsoleState +{ + Disconnected, + Connecting, + Connected +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Enums/ServerState.cs b/Moonlight/App/Helpers/Wings/Enums/ServerState.cs new file mode 100644 index 00000000..1ced0020 --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Enums/ServerState.cs @@ -0,0 +1,10 @@ +namespace Moonlight.App.Helpers.Wings.Enums; + +public enum ServerState +{ + Starting, + Running, + Stopping, + Offline, + Installing +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Events/BaseEvent.cs b/Moonlight/App/Helpers/Wings/Events/BaseEvent.cs new file mode 100644 index 00000000..9c0455bd --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Events/BaseEvent.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Helpers.Wings.Events; + +public class BaseEvent +{ + public string Event { get; set; } = ""; + public string[] Args { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Events/SendTokenEvent.cs b/Moonlight/App/Helpers/Wings/Events/SendTokenEvent.cs new file mode 100644 index 00000000..660ae448 --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Events/SendTokenEvent.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Helpers.Wings.Events; + +public class SendTokenEvent +{ + public string Event { get; set; } = "auth"; + public List Args = new(); +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/WingsConsole.cs b/Moonlight/App/Helpers/Wings/WingsConsole.cs new file mode 100644 index 00000000..c51910a8 --- /dev/null +++ b/Moonlight/App/Helpers/Wings/WingsConsole.cs @@ -0,0 +1,389 @@ +using System.Net.WebSockets; +using System.Text; +using Logging.Net; +using Moonlight.App.Helpers.Wings.Data; +using Moonlight.App.Helpers.Wings.Enums; +using Moonlight.App.Helpers.Wings.Events; +using Newtonsoft.Json; +using ConsoleMessage = Moonlight.App.Helpers.Wings.Data.ConsoleMessage; + +namespace Moonlight.App.Helpers.Wings; + +public class WingsConsole : IDisposable +{ + private ClientWebSocket WebSocket; + public List Messages; + private Task? ConsoleTask; + + private string Socket = ""; + private string Origin = ""; + private string Token = ""; + + private bool Disconnecting; + + public ConsoleState ConsoleState { get; private set; } + public ServerState ServerState { get; private set; } + public ServerResource Resource { get; private set; } + + public EventHandler OnConsoleStateUpdated { get; set; } + public EventHandler OnServerStateUpdated { get; set; } + public EventHandler OnResourceUpdated { get; set; } + public EventHandler OnMessage { get; set; } + public Func> OnRequestNewToken { get; set; } + + public WingsConsole() + { + ConsoleState = ConsoleState.Disconnected; + ServerState = ServerState.Offline; + Messages = new(); + + Resource = new() + { + Network = new() + { + RxBytes = 0, + TxBytes = 0 + }, + State = "offline", + Uptime = 0, + CpuAbsolute = 0, + DiskBytes = 0, + MemoryBytes = 0, + MemoryLimitBytes = 0 + }; + } + + public Task Connect(string origin, string socket, string token) + { + Disconnecting = false; + WebSocket = new(); + ConsoleState = ConsoleState.Disconnected; + ServerState = ServerState.Offline; + Messages = new(); + + Resource = new() + { + Network = new() + { + RxBytes = 0, + TxBytes = 0 + }, + State = "offline", + Uptime = 0, + CpuAbsolute = 0, + DiskBytes = 0, + MemoryBytes = 0, + MemoryLimitBytes = 0 + }; + + Socket = socket; + Origin = origin; + Token = token; + + WebSocket.Options.SetRequestHeader("Origin", Origin); + WebSocket.Options.SetRequestHeader("Authorization", "Bearer " + Token); + + ConsoleTask = Task.Run(async () => + { + try + { + await Work(); + } + catch (Exception e) + { + Logger.Warn("Error connecting to wings console"); + Logger.Warn(e); + } + }); + + return Task.CompletedTask; + } + + private async Task Work() + { + await UpdateConsoleState(ConsoleState.Connecting); + + await WebSocket.ConnectAsync( + new Uri(Socket), + CancellationToken.None + ); + + if (WebSocket.State != WebSocketState.Connecting && WebSocket.State != WebSocketState.Open) + { + await SaveMessage("Unable to connect to websocket", true); + await UpdateConsoleState(ConsoleState.Disconnected); + return; + } + + await UpdateConsoleState(ConsoleState.Connected); + + await Send(new SendTokenEvent() + { + Args = { Token } + }); + + while (WebSocket.State == WebSocketState.Open) + { + try + { + var raw = await ReceiveRaw(); + + if(string.IsNullOrEmpty(raw)) + continue; + + var eventData = JsonConvert.DeserializeObject(raw); + + if (eventData == null) + { + await SaveMessage("Unable to parse event", true); + continue; + } + + switch (eventData.Event) + { + case "jwt error": + if (WebSocket != null) + { + if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open) + await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + + WebSocket.Dispose(); + } + + await UpdateServerState(ServerState.Offline); + await UpdateConsoleState(ConsoleState.Disconnected); + + await SaveMessage("Received a jwt error. Disconnected", true); + break; + + case "token expired": + if (WebSocket != null) + { + if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open) + await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + + WebSocket.Dispose(); + } + + await UpdateServerState(ServerState.Offline); + await UpdateConsoleState(ConsoleState.Disconnected); + + await SaveMessage("Token expired", true); + + break; + + case "token expiring": + await SaveMessage("Token will expire soon. Generating a new one", true); + + Token = await OnRequestNewToken.Invoke(this); + + await Send(new SendTokenEvent() + { + Args = { Token } + }); + break; + + case "auth success": + // Send intents + await SendRaw("{\"event\":\"send logs\",\"args\":[null]}"); + await SendRaw("{\"event\":\"send stats\",\"args\":[null]}"); + break; + + case "stats": + var stats = JsonConvert.DeserializeObject(eventData.Args[0]); + + if (stats == null) + break; + + var serverState = ParseServerState(stats.State); + + if (ServerState != serverState) + await UpdateServerState(serverState); + + await UpdateResource(stats); + break; + + case "status": + var serverStateParsed = ParseServerState(eventData.Args[0]); + + if (ServerState != serverStateParsed) + await UpdateServerState(serverStateParsed); + break; + + case "console output": + foreach (var line in eventData.Args) + { + await SaveMessage(line); + } + + break; + + case "install output": + foreach (var line in eventData.Args) + { + await SaveMessage(line); + } + + break; + + case "daemon message": + foreach (var line in eventData.Args) + { + await SaveMessage(line); + } + + break; + + case "install started": + await UpdateServerState(ServerState.Installing); + break; + + case "install completed": + await UpdateServerState(ServerState.Offline); + break; + } + } + catch (Exception e) + { + if (!Disconnecting) + { + Logger.Warn("Error while performing websocket actions"); + Logger.Warn(e); + + await SaveMessage("A unknown error occured while processing websocket", true); + } + } + } + } + + private Task UpdateConsoleState(ConsoleState consoleState) + { + ConsoleState = consoleState; + OnConsoleStateUpdated?.Invoke(this, consoleState); + + return Task.CompletedTask; + } + private Task UpdateServerState(ServerState serverState) + { + ServerState = serverState; + OnServerStateUpdated?.Invoke(this, serverState); + + return Task.CompletedTask; + } + private Task UpdateResource(ServerResource resource) + { + Resource = resource; + OnResourceUpdated?.Invoke(this, Resource); + + return Task.CompletedTask; + } + + private Task SaveMessage(string content, bool internalMessage = false) + { + var msg = new ConsoleMessage() + { + Content = content, + IsInternal = internalMessage + }; + + lock (Messages) + { + Messages.Add(msg); + } + + OnMessage?.Invoke(this, msg); + + return Task.CompletedTask; + } + + private ServerState ParseServerState(string raw) + { + switch (raw) + { + case "offline": + return ServerState.Offline; + case "starting": + return ServerState.Starting; + case "running": + return ServerState.Running; + case "stopping": + return ServerState.Stopping; + case "installing": + return ServerState.Installing; + default: + return ServerState.Offline; + } + } + + public async Task EnterCommand(string content) + { + if (ConsoleState == ConsoleState.Connected) + { + await SendRaw("{\"event\":\"send command\",\"args\":[\"" + content + "\"]}"); + } + } + + public async Task SetPowerState(string state) + { + if (ConsoleState == ConsoleState.Connected) + { + await SendRaw("{\"event\":\"set state\",\"args\":[\"" + state + "\"]}"); + } + } + + private async Task Send(object data) + { + await SendRaw(JsonConvert.SerializeObject(data)); + } + + private async Task SendRaw(string data) + { + if (WebSocket.State == WebSocketState.Open) + { + byte[] byteContentBuffer = Encoding.UTF8.GetBytes(data); + await WebSocket.SendAsync(new ArraySegment(byteContentBuffer), WebSocketMessageType.Text, true, + CancellationToken.None); + } + } + + private async Task ReceiveRaw() + { + ArraySegment receivedBytes = new ArraySegment(new byte[1024]); + WebSocketReceiveResult result = await WebSocket.ReceiveAsync(receivedBytes, CancellationToken.None); + return Encoding.UTF8.GetString(receivedBytes.Array!, 0, result.Count); + } + + public async Task Disconnect() + { + Disconnecting = true; + Messages.Clear(); + + if (WebSocket != null) + { + if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open) + await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + + WebSocket.Dispose(); + } + + if(ConsoleTask != null && ConsoleTask.IsCompleted) + ConsoleTask.Dispose(); + } + + public void Dispose() + { + Disconnecting = true; + Messages.Clear(); + + if (WebSocket != null) + { + if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open) + WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None).Wait(); + + WebSocket.Dispose(); + } + + if(ConsoleTask != null && ConsoleTask.IsCompleted) + ConsoleTask.Dispose(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/WingsConsoleHelper.cs b/Moonlight/App/Helpers/Wings/WingsConsoleHelper.cs similarity index 83% rename from Moonlight/App/Helpers/WingsConsoleHelper.cs rename to Moonlight/App/Helpers/Wings/WingsConsoleHelper.cs index 720a293b..af2fc7d5 100644 --- a/Moonlight/App/Helpers/WingsConsoleHelper.cs +++ b/Moonlight/App/Helpers/Wings/WingsConsoleHelper.cs @@ -7,37 +7,34 @@ using Moonlight.App.Database.Entities; using Moonlight.App.Repositories.Servers; using Moonlight.App.Services; -namespace Moonlight.App.Helpers; +namespace Moonlight.App.Helpers.Wings; public class WingsConsoleHelper { private readonly ServerRepository ServerRepository; - private readonly WingsJwtHelper WingsJwtHelper; private readonly string AppUrl; public WingsConsoleHelper( ServerRepository serverRepository, - ConfigService configService, - WingsJwtHelper wingsJwtHelper) + ConfigService configService) { ServerRepository = serverRepository; - WingsJwtHelper = wingsJwtHelper; AppUrl = configService.GetSection("Moonlight").GetValue("AppUrl"); } - public async Task ConnectWings(PteroConsole.NET.PteroConsole pteroConsole, Server server) + public async Task ConnectWings(WingsConsole console, Server server) { var serverData = ServerRepository .Get() .Include(x => x.Node) .First(x => x.Id == server.Id); - - var token = GenerateToken(serverData); + + var token = await GenerateToken(serverData); if (serverData.Node.Ssl) { - await pteroConsole.Connect( + await console.Connect( AppUrl, $"wss://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws", token @@ -45,7 +42,7 @@ public class WingsConsoleHelper } else { - await pteroConsole.Connect( + await console.Connect( AppUrl, $"ws://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws", token @@ -53,20 +50,20 @@ public class WingsConsoleHelper } } - public string GenerateToken(Server server) + public async Task 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 inputBytes = Encoding.ASCII.GetBytes(userid + server.Uuid.ToString()); var outputBytes = md5.ComputeHash(inputBytes); var identifier = Convert.ToHexString(outputBytes).ToLower(); @@ -77,7 +74,7 @@ public class WingsConsoleHelper .WithAlgorithm(new HMACSHA256Algorithm()) .WithSecret(secret) .AddClaim("user_id", userid) - .AddClaim("server_uuid", serverData.Uuid.ToString()) + .AddClaim("server_uuid", server.Uuid.ToString()) .AddClaim("permissions", new[] { "*", diff --git a/Moonlight/App/Helpers/WingsJwtHelper.cs b/Moonlight/App/Helpers/Wings/WingsJwtHelper.cs similarity index 97% rename from Moonlight/App/Helpers/WingsJwtHelper.cs rename to Moonlight/App/Helpers/Wings/WingsJwtHelper.cs index 221abd1f..8e4af78a 100644 --- a/Moonlight/App/Helpers/WingsJwtHelper.cs +++ b/Moonlight/App/Helpers/Wings/WingsJwtHelper.cs @@ -4,7 +4,7 @@ using JWT.Algorithms; using JWT.Builder; using Moonlight.App.Services; -namespace Moonlight.App.Helpers; +namespace Moonlight.App.Helpers.Wings; public class WingsJwtHelper { diff --git a/Moonlight/App/Helpers/WingsServerConverter.cs b/Moonlight/App/Helpers/Wings/WingsServerConverter.cs similarity index 99% rename from Moonlight/App/Helpers/WingsServerConverter.cs rename to Moonlight/App/Helpers/Wings/WingsServerConverter.cs index c7375e3d..c0875ab1 100644 --- a/Moonlight/App/Helpers/WingsServerConverter.cs +++ b/Moonlight/App/Helpers/Wings/WingsServerConverter.cs @@ -5,7 +5,7 @@ using Moonlight.App.Http.Resources.Wings; using Moonlight.App.Repositories; using Moonlight.App.Repositories.Servers; -namespace Moonlight.App.Helpers; +namespace Moonlight.App.Helpers.Wings; public class WingsServerConverter { diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/DiscordBotController.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/DiscordBotController.cs new file mode 100644 index 00000000..d2c4adb4 --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/DiscordBotController.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.ApiClients.Wings; +using Moonlight.App.ApiClients.Wings.Resources; +using Moonlight.App.Database.Entities; +using Moonlight.App.Http.Requests.DiscordBot.Requests; +using Moonlight.App.Repositories; +using Moonlight.App.Services; + +namespace Moonlight.App.Http.Controllers.Api.Moonlight; + +[ApiController] +[Route("api/moonlight/discordbot")] +public class DiscordBotController : Controller +{ + private readonly Repository UserRepository; + private readonly Repository ServerRepository; + private readonly ServerService ServerService; + private readonly string Token = ""; + private readonly bool Enable; + + public DiscordBotController( + Repository userRepository, + Repository serverRepository, + ServerService serverService, + ConfigService configService) + { + UserRepository = userRepository; + ServerRepository = serverRepository; + ServerService = serverService; + + var config = configService + .GetSection("Moonlight") + .GetSection("DiscordBotApi"); + + Enable = config.GetValue("Enable"); + + if (Enable) + { + Token = config.GetValue("Token"); + } + } + + [HttpGet("{id}/link")] + public async Task GetLink(ulong id) + { + if (!await IsAuth(Request)) + return StatusCode(403); + + if (await GetUserFromDiscordId(id) == null) + { + return BadRequest(); + } + + return Ok(); + } + + [HttpGet("{id}/servers")] + public async Task> GetServers(ulong id) + { + if (!await IsAuth(Request)) + return StatusCode(403); + + var user = await GetUserFromDiscordId(id); + + if (user == null) + return BadRequest(); + + return ServerRepository + .Get() + .Include(x => x.Owner) + .Include(x => x.Image) + .Where(x => x.Owner.Id == user.Id) + .ToArray(); + } + + [HttpPost("{id}/servers/{uuid}")] + public async Task SetPowerState(ulong id, Guid uuid, [FromBody] SetPowerSignal signal) + { + if (!await IsAuth(Request)) + return StatusCode(403); + + var user = await GetUserFromDiscordId(id); + + if (user == null) + return BadRequest(); + + var server = ServerRepository + .Get() + .Include(x => x.Owner) + .FirstOrDefault(x => x.Owner.Id == user.Id && x.Uuid == uuid); + + if (server == null) + return NotFound(); + + if (Enum.TryParse(signal.Signal, true, out PowerSignal powerSignal)) + { + await ServerService.SetPowerState(server, powerSignal); + return Ok(); + } + else + return BadRequest(); + } + + [HttpGet("{id}/servers/{uuid}")] + public async Task> GetServerDetails(ulong id, Guid uuid) + { + if (!await IsAuth(Request)) + return StatusCode(403); + + var user = await GetUserFromDiscordId(id); + + if (user == null) + return BadRequest(); + + var server = ServerRepository + .Get() + .Include(x => x.Owner) + .FirstOrDefault(x => x.Owner.Id == user.Id && x.Uuid == uuid); + + if (server == null) + return NotFound(); + + return await ServerService.GetDetails(server); + } + + private Task GetUserFromDiscordId(ulong discordId) + { + var user = UserRepository + .Get() + .FirstOrDefault(x => x.DiscordId == discordId); + + return Task.FromResult(user); + } + + private Task IsAuth(HttpRequest request) + { + if (!Enable) + return Task.FromResult(false); + + if (string.IsNullOrEmpty(request.Headers.Authorization)) + return Task.FromResult(false); + + if(request.Headers.Authorization == Token) + return Task.FromResult(true); + + return Task.FromResult(false); + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs index 5fc50874..9cff03d0 100644 --- a/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs @@ -1,6 +1,7 @@ using Logging.Net; using Microsoft.AspNetCore.Mvc; using Moonlight.App.Services; +using Moonlight.App.Services.Sessions; namespace Moonlight.App.Http.Controllers.Api.Moonlight; @@ -11,12 +12,41 @@ public class OAuth2Controller : Controller private readonly UserService UserService; private readonly OAuth2Service OAuth2Service; private readonly DateTimeService DateTimeService; + private readonly IdentityService IdentityService; - public OAuth2Controller(UserService userService, OAuth2Service oAuth2Service, DateTimeService dateTimeService) + public OAuth2Controller( + UserService userService, + OAuth2Service oAuth2Service, + DateTimeService dateTimeService, + IdentityService identityService) { UserService = userService; OAuth2Service = oAuth2Service; DateTimeService = dateTimeService; + IdentityService = identityService; + } + + [HttpGet("{id}/start")] + public async Task Start([FromRoute] string id) + { + try + { + if (OAuth2Service.Providers.ContainsKey(id)) + { + return Redirect(await OAuth2Service.GetUrl(id)); + } + + Logger.Warn($"Someone tried to start an oauth2 flow using the id '{id}' which is not registered"); + + return Redirect("/"); + } + catch (Exception e) + { + Logger.Warn($"Error starting oauth2 flow for id: {id}"); + Logger.Warn(e); + + return Redirect("/"); + } } [HttpGet("{id}")] @@ -24,6 +54,18 @@ public class OAuth2Controller : Controller { try { + var currentUser = await IdentityService.Get(); + + if (currentUser != null) + { + if (await OAuth2Service.CanBeLinked(id)) + { + await OAuth2Service.LinkToUser(id, currentUser, code); + + return Redirect("/profile"); + } + } + var user = await OAuth2Service.HandleCode(id, code); Response.Cookies.Append("token", await UserService.GenerateToken(user), new() diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs index 6632386c..d701a2c2 100644 --- a/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs @@ -16,14 +16,12 @@ public class ResourcesController : Controller { private readonly SecurityLogService SecurityLogService; private readonly BucketService BucketService; - private readonly BundleService BundleService; public ResourcesController(SecurityLogService securityLogService, - BucketService bucketService, BundleService bundleService) + BucketService bucketService) { SecurityLogService = securityLogService; BucketService = bucketService; - BundleService = bundleService; } [HttpGet("images/{name}")] @@ -77,34 +75,4 @@ public class ResourcesController : Controller return Problem(); } } - - [HttpGet("bundle/js")] - public Task GetJs() - { - if (BundleService.BundledFinished) - { - return Task.FromResult( - File(Encoding.ASCII.GetBytes(BundleService.BundledJs), "text/javascript") - ); - } - - return Task.FromResult( - NotFound() - ); - } - - [HttpGet("bundle/css")] - public Task GetCss() - { - if (BundleService.BundledFinished) - { - return Task.FromResult( - File(Encoding.ASCII.GetBytes(BundleService.BundledCss), "text/css") - ); - } - - return Task.FromResult( - NotFound() - ); - } } \ 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 index 08f4ddc0..b7afee2e 100644 --- a/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs +++ b/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Moonlight.App.Events; using Moonlight.App.Helpers; +using Moonlight.App.Helpers.Wings; using Moonlight.App.Http.Resources.Wings; using Moonlight.App.Repositories; using Moonlight.App.Repositories.Servers; diff --git a/Moonlight/App/Http/Requests/DiscordBot/Requests/SetPowerSignal.cs b/Moonlight/App/Http/Requests/DiscordBot/Requests/SetPowerSignal.cs new file mode 100644 index 00000000..1d364053 --- /dev/null +++ b/Moonlight/App/Http/Requests/DiscordBot/Requests/SetPowerSignal.cs @@ -0,0 +1,8 @@ +using Moonlight.App.ApiClients.Wings; + +namespace Moonlight.App.Http.Requests.DiscordBot.Requests; + +public class SetPowerSignal +{ + public string Signal { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/OAuth2/OAuth2Provider.cs b/Moonlight/App/OAuth2/OAuth2Provider.cs index af582c13..bad02d7e 100644 --- a/Moonlight/App/OAuth2/OAuth2Provider.cs +++ b/Moonlight/App/OAuth2/OAuth2Provider.cs @@ -9,7 +9,9 @@ public abstract class OAuth2Provider public string Url { get; set; } public IServiceScopeFactory ServiceScopeFactory { get; set; } public string DisplayName { get; set; } + public bool CanBeLinked { get; set; } = false; public abstract Task GetUrl(); public abstract Task HandleCode(string code); + public abstract Task LinkToUser(User user, string code); } \ No newline at end of file diff --git a/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs b/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs index 5ed2efea..ffe58b29 100644 --- a/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs +++ b/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs @@ -12,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers; public class DiscordOAuth2Provider : OAuth2Provider { + public DiscordOAuth2Provider() + { + CanBeLinked = true; + } + public override Task GetUrl() { string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" + @@ -119,4 +124,74 @@ public class DiscordOAuth2Provider : OAuth2Provider return user; } } + + public override async Task LinkToUser(User user, string code) + { + // Endpoints + + var endpoint = Url + "/api/moonlight/oauth2/discord"; + var discordUserDataEndpoint = "https://discordapp.com/api/users/@me"; + var discordEndpoint = "https://discordapp.com/api/oauth2/token"; + + // Generate access token + + using var client = new RestClient(); + var request = new RestRequest(discordEndpoint); + + request.AddParameter("client_id", Config.ClientId); + request.AddParameter("client_secret", Config.ClientSecret); + request.AddParameter("grant_type", "authorization_code"); + request.AddParameter("code", code); + request.AddParameter("redirect_uri", endpoint); + + var response = await client.ExecutePostAsync(request); + + if (!response.IsSuccessful) + { + Logger.Warn("Error verifying oauth2 code"); + Logger.Warn(response.ErrorMessage); + throw new DisplayException("An error occured while verifying oauth2 code"); + } + + // parse response + + var data = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(response.Content!)) + ).Build(); + + var accessToken = data.GetValue("access_token"); + + // Now, we will call the discord api with our access token to get the data we need + + var getRequest = new RestRequest(discordUserDataEndpoint); + getRequest.AddHeader("Authorization", $"Bearer {accessToken}"); + + var getResponse = await client.ExecuteGetAsync(getRequest); + + if (!getResponse.IsSuccessful) + { + Logger.Warn("An unexpected error occured while fetching user data from remote api"); + Logger.Warn(getResponse.ErrorMessage); + + throw new DisplayException("An unexpected error occured while fetching user data from remote api"); + } + + // Parse response + + var getData = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(getResponse.Content!)) + ).Build(); + + var id = getData.GetValue("id"); + + // Handle data + + using var scope = ServiceScopeFactory.CreateScope(); + + var userRepo = scope.ServiceProvider.GetRequiredService>(); + + user.DiscordId = id; + + userRepo.Update(user); + } } \ No newline at end of file diff --git a/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs b/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs index 72758542..c0e56d55 100644 --- a/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs +++ b/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs @@ -4,7 +4,6 @@ using Moonlight.App.ApiClients.Google.Requests; using Moonlight.App.Database.Entities; using Moonlight.App.Exceptions; using Moonlight.App.Helpers; -using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; using Moonlight.App.Services; using RestSharp; @@ -13,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers; public class GoogleOAuth2Provider : OAuth2Provider { + public GoogleOAuth2Provider() + { + CanBeLinked = false; + } + public override Task GetUrl() { var endpoint = Url + "/api/moonlight/oauth2/google"; @@ -127,4 +131,9 @@ public class GoogleOAuth2Provider : OAuth2Provider return user; } } + + public override Task LinkToUser(User user, string code) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Moonlight/App/Services/OAuth2Service.cs b/Moonlight/App/Services/OAuth2Service.cs index c96bd497..31f17ff4 100644 --- a/Moonlight/App/Services/OAuth2Service.cs +++ b/Moonlight/App/Services/OAuth2Service.cs @@ -80,6 +80,26 @@ public class OAuth2Service return await provider.HandleCode(code); } + public Task CanBeLinked(string id) + { + if (Providers.All(x => x.Key != id)) + throw new DisplayException("Invalid oauth2 id"); + + var provider = Providers[id]; + + return Task.FromResult(provider.CanBeLinked); + } + + public async Task LinkToUser(string id, User user, string code) + { + if (Providers.All(x => x.Key != id)) + throw new DisplayException("Invalid oauth2 id"); + + var provider = Providers[id]; + + await provider.LinkToUser(user, code); + } + private string GetAppUrl() { if (EnableOverrideUrl) diff --git a/Moonlight/App/Services/ServerService.cs b/Moonlight/App/Services/ServerService.cs index fb9c4db0..ae0390b0 100644 --- a/Moonlight/App/Services/ServerService.cs +++ b/Moonlight/App/Services/ServerService.cs @@ -8,6 +8,7 @@ using Moonlight.App.Events; using Moonlight.App.Exceptions; using Moonlight.App.Helpers; using Moonlight.App.Helpers.Files; +using Moonlight.App.Helpers.Wings; using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; using Moonlight.App.Repositories.Servers; diff --git a/Moonlight/App/Services/Sessions/BundleService.cs b/Moonlight/App/Services/Sessions/BundleService.cs deleted file mode 100644 index 2b0cfbfc..00000000 --- a/Moonlight/App/Services/Sessions/BundleService.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Logging.Net; - -namespace Moonlight.App.Services.Sessions; - -public class BundleService -{ - public BundleService(ConfigService configService) - { - var url = configService - .GetSection("Moonlight") - .GetValue("AppUrl"); - - #region JS - - JsFiles = new(); - - JsFiles.AddRange(new[] - { - url + "/_framework/blazor.server.js", - url + "/assets/plugins/global/plugins.bundle.js", - url + "/_content/XtermBlazor/XtermBlazor.min.js", - url + "/_content/BlazorTable/BlazorTable.min.js", - url + "/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js", - url + "/_content/Blazor.ContextMenu/blazorContextMenu.min.js", - "https://www.google.com/recaptcha/api.js", - "https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.min.js", - "https://cdn.jsdelivr.net/npm/xterm-addon-search@0.8.2/lib/xterm-addon-search.min.js", - "https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.5.0/lib/xterm-addon-web-links.min.js", - url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js", - "require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });", - url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js", - url + "/_content/BlazorMonaco/jsInterop.js", - url + "/assets/js/scripts.bundle.js", - url + "/assets/js/moonlight.js", - "moonlight.loading.registerXterm();", - url + "/_content/Blazor-ApexCharts/js/apex-charts.min.js", - url + "/_content/Blazor-ApexCharts/js/blazor-apex-charts.js" - }); - - #endregion - - #region CSS - - CssFiles = new(); - - CssFiles.AddRange(new[] - { - "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700", - url + "/assets/css/style.bundle.css", - url + "/assets/css/flashbang.css", - url + "/assets/css/snow.css", - url + "/assets/css/utils.css", - url + "/assets/css/blazor.css", - url + "/_content/XtermBlazor/XtermBlazor.css", - url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css", - url + "/_content/Blazor.ContextMenu/blazorContextMenu.min.css", - url + "/assets/plugins/global/plugins.bundle.css" - }); - - #endregion - - CacheId = Guid.NewGuid().ToString(); - - Task.Run(Bundle); - } - - // Javascript - public string BundledJs { get; private set; } - public readonly List JsFiles; - - // CSS - public string BundledCss { get; private set; } - public readonly List CssFiles; - - // States - public string CacheId { get; private set; } - public bool BundledFinished { get; set; } = false; - private bool IsBundling { get; set; } = false; - - private async Task Bundle() - { - if (!IsBundling) - IsBundling = true; - - Logger.Info("Bundling js and css files"); - - BundledJs = ""; - BundledCss = ""; - - BundledJs = await BundleFiles( - JsFiles - ); - - BundledCss = await BundleFiles( - CssFiles - ); - - Logger.Info("Successfully bundled"); - BundledFinished = true; - } - - private async Task BundleFiles(IEnumerable items) - { - var bundled = ""; - - using HttpClient client = new HttpClient(); - foreach (string item in items) - { - // Item is a url, fetch it - if (item.StartsWith("http")) - { - try - { - var jsCode = await client.GetStringAsync(item); - bundled += jsCode + "\n"; - } - catch (Exception e) - { - Logger.Warn($"Error fetching '{item}' while bundling"); - Logger.Warn(e); - } - } - else // If not, it is probably a manual addition, so add it - bundled += item + "\n"; - } - - return bundled; - } -} \ No newline at end of file diff --git a/Moonlight/App/Services/SmartDeployService.cs b/Moonlight/App/Services/SmartDeployService.cs index 2690d56e..3aaaf4c1 100644 --- a/Moonlight/App/Services/SmartDeployService.cs +++ b/Moonlight/App/Services/SmartDeployService.cs @@ -9,21 +9,36 @@ public class SmartDeployService private readonly Repository CloudPanelRepository; private readonly WebSpaceService WebSpaceService; private readonly NodeService NodeService; + private readonly ConfigService ConfigService; public SmartDeployService( NodeRepository nodeRepository, NodeService nodeService, WebSpaceService webSpaceService, - Repository cloudPanelRepository) + Repository cloudPanelRepository, + ConfigService configService) { NodeRepository = nodeRepository; NodeService = nodeService; WebSpaceService = webSpaceService; CloudPanelRepository = cloudPanelRepository; + ConfigService = configService; } public async Task GetNode() { + var config = ConfigService + .GetSection("Moonlight") + .GetSection("SmartDeploy") + .GetSection("Server"); + + if (config.GetValue("EnableOverride")) + { + var nodeId = config.GetValue("OverrideNode"); + + return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId); + } + var data = new Dictionary(); foreach (var node in NodeRepository.Get().ToArray()) diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 5b85d56f..7531db6e 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -41,7 +41,6 @@ - diff --git a/Moonlight/Pages/_Layout.cshtml b/Moonlight/Pages/_Layout.cshtml index 975533a9..73ba593c 100644 --- a/Moonlight/Pages/_Layout.cshtml +++ b/Moonlight/Pages/_Layout.cshtml @@ -7,7 +7,6 @@ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @inject ConfigService ConfigService -@inject BundleService BundleService @inject LoadingMessageRepository LoadingMessageRepository @{ @@ -38,29 +37,20 @@ - @*This import is not in the bundle because the files it references are linked relative to the current lath*@ + + + + + + - - @if (BundleService.BundledFinished) - { - - } - else - { - foreach (var cssFile in BundleService.CssFiles) - { - if (cssFile.StartsWith("http")) - { - - } - else - { - - } - } - } + + + + + + + @@ -106,24 +96,33 @@ -@if (BundleService.BundledFinished) -{ - -} -else -{ - foreach (var jsFile in BundleService.JsFiles) - { - if (jsFile.StartsWith("http")) - { - - } - else - { - @Html.Raw("") - } - } -} + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index d9a46b8e..a3795997 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -9,6 +9,7 @@ using Moonlight.App.ApiClients.Wings; using Moonlight.App.Database; using Moonlight.App.Events; using Moonlight.App.Helpers; +using Moonlight.App.Helpers.Wings; using Moonlight.App.LogMigrator; using Moonlight.App.Repositories; using Moonlight.App.Repositories.Domains; @@ -127,7 +128,6 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor b/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor index cedca894..5925c9d2 100644 --- a/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor +++ b/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor @@ -185,6 +185,8 @@ else await ToastService.Info(SmartTranslateService.Translate("Starting download")); } } + else + await ToastService.Error(SmartTranslateService.Translate("You are not able to download folders using the moonlight file manager")); } }); diff --git a/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor b/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor index ab04ef6e..dbf3bea5 100644 --- a/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor @@ -9,7 +9,7 @@
@(User.FirstName) @(User.LastName) - + @if (User.Status == UserStatus.Verified) { @@ -31,12 +31,17 @@ + diff --git a/Moonlight/Shared/Components/ServerControl/ServerBackups.razor b/Moonlight/Shared/Components/ServerControl/ServerBackups.razor index 7175a3c7..fc71c474 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerBackups.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerBackups.razor @@ -1,6 +1,4 @@ -@using PteroConsole.NET -@using Moonlight.App.Services -@using Task = System.Threading.Tasks.Task +@using Moonlight.App.Services @using Moonlight.App.Helpers @using Logging.Net @using BlazorContextMenu @@ -101,9 +99,6 @@ @code { - [CascadingParameter] - public PteroConsole Console { get; set; } - [CascadingParameter] public Server CurrentServer { get; set; } diff --git a/Moonlight/Shared/Components/ServerControl/ServerConsole.razor b/Moonlight/Shared/Components/ServerControl/ServerConsole.razor index 474efa29..454e6d87 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerConsole.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerConsole.razor @@ -1,11 +1,9 @@ -@using PteroConsole.NET -@using PteroConsole.NET.Enums -@using Task = System.Threading.Tasks.Task -@using Moonlight.App.Helpers +@using Moonlight.App.Helpers @using Moonlight.App.Repositories @using Moonlight.App.Services -@using Logging.Net @using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers.Wings +@using Moonlight.App.Helpers.Wings.Data @using Moonlight.App.Services.Interop @using Moonlight.Shared.Components.Xterm @@ -37,7 +35,7 @@ @code { [CascadingParameter] - public PteroConsole Console { get; set; } + public WingsConsole Console { get; set; } [CascadingParameter] public Server CurrentServer { get; set; } @@ -51,21 +49,35 @@ Console.OnMessage += OnMessage; } - private async void OnMessage(object? sender, string e) + private async void OnMessage(object? sender, ConsoleMessage message) { if (Terminal != null) { - var s = e; + if (message.IsInternal) + { + await Terminal.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + message.Content + "\x1b[0m"); + } + else + { + var s = message.Content; - s = s.Replace("Pterodactyl Daemon", "Moonlight Daemon"); - s = s.Replace("Checking server disk space usage, this could take a few seconds...", TranslationService.Translate("Checking disk space")); - s = s.Replace("Updating process configuration files...", TranslationService.Translate("Updating config files")); - s = s.Replace("Ensuring file permissions are set correctly, this could take a few seconds...", TranslationService.Translate("Checking file permissions")); - s = s.Replace("Pulling Docker container image, this could take a few minutes to complete...", TranslationService.Translate("Downloading server image")); - s = s.Replace("Finished pulling Docker container image", TranslationService.Translate("Downloaded server image")); - s = s.Replace("container@pterodactyl~", "server@moonlight >"); + if (s.Contains("Moonlight Daemon") || s.Contains("Pterodactyl Daemon")) + { + s = s.Replace("[39m", "\x1b[0m"); + s = s.Replace("[33m", "[38;5;16;48;5;135m\x1b[39m"); + } - await Terminal.WriteLine(s); + s = s.Replace("[Pterodactyl Daemon]:", " Moonlight "); + s = s.Replace("[Moonlight Daemon]:", " Moonlight "); + s = s.Replace("Checking server disk space usage, this could take a few seconds...", TranslationService.Translate("Checking disk space")); + s = s.Replace("Updating process configuration files...", TranslationService.Translate("Updating config files")); + s = s.Replace("Ensuring file permissions are set correctly, this could take a few seconds...", TranslationService.Translate("Checking file permissions")); + s = s.Replace("Pulling Docker container image, this could take a few minutes to complete...", TranslationService.Translate("Downloading server image")); + s = s.Replace("Finished pulling Docker container image", TranslationService.Translate("Downloaded server image")); + s = s.Replace("container@pterodactyl~", "server@moonlight >"); + + await Terminal.WriteLine(s); + } } } @@ -85,9 +97,9 @@ private void RunOnFirstRender() { - lock (Console.MessageCache) + lock (Console.Messages) { - foreach (var message in Console.MessageCache.TakeLast(30)) + foreach (var message in Console.Messages) { OnMessage(null, message); } diff --git a/Moonlight/Shared/Components/ServerControl/ServerFiles.razor b/Moonlight/Shared/Components/ServerControl/ServerFiles.razor index 6267a9b8..9ce54cd2 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerFiles.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerFiles.razor @@ -4,6 +4,7 @@ @using Moonlight.App.Helpers.Files @using Moonlight.App.Services @using Moonlight.App.ApiClients.Wings +@using Moonlight.App.Helpers.Wings @inject WingsApiHelper WingsApiHelper @inject WingsJwtHelper WingsJwtHelper diff --git a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor index 4d10f7b2..aff316a3 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor @@ -1,9 +1,8 @@ -@using PteroConsole.NET -@using PteroConsole.NET.Enums -@using Task = System.Threading.Tasks.Task -@using Moonlight.App.Services +@using Moonlight.App.Services @using Moonlight.App.Database.Entities @using Moonlight.App.Helpers +@using Moonlight.App.Helpers.Wings +@using Moonlight.App.Helpers.Wings.Enums @inject SmartTranslateService TranslationService @@ -77,32 +76,32 @@ case ServerState.Starting: Starting - (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + (@(Formatter.FormatUptime(Console.Resource.Uptime))) break; case ServerState.Stopping: Stopping - (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + (@(Formatter.FormatUptime(Console.Resource.Uptime))) break; case ServerState.Running: Online - (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + (@(Formatter.FormatUptime(Console.Resource.Uptime))) break; }
Cpu: - @(Math.Round(Console.ServerResource.CpuAbsolute, 2))% + @(Math.Round(Console.Resource.CpuAbsolute / (CurrentServer.Cpu / 100f), 2))%
Memory: - @(Formatter.FormatSize(Console.ServerResource.MemoryBytes)) / @(Formatter.FormatSize(Console.ServerResource.MemoryLimitBytes)) + @(Formatter.FormatSize(Console.Resource.MemoryBytes)) / @(Formatter.FormatSize(Console.Resource.MemoryLimitBytes))
Disk: - @(Formatter.FormatSize(Console.ServerResource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB + @(Formatter.FormatSize(Console.Resource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB
@@ -178,7 +177,7 @@ public User User { get; set; } [CascadingParameter] - public PteroConsole Console { get; set; } + public WingsConsole Console { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } @@ -190,8 +189,8 @@ protected override void OnInitialized() { - Console.OnServerStateUpdated += async (sender, state) => { await InvokeAsync(StateHasChanged); }; - Console.OnServerResourceUpdated += async (sender, x) => { await InvokeAsync(StateHasChanged); }; + Console.OnServerStateUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; + Console.OnResourceUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; } #region Power Actions diff --git a/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor b/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor index 54254ed4..21c0df49 100644 --- a/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor +++ b/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor @@ -5,9 +5,11 @@ @using Moonlight.App.Repositories @using Moonlight.App.Repositories.Servers @using Logging.Net +@using Moonlight.App.ApiClients.Wings @using Moonlight.App.Database.Entities @inject ServerRepository ServerRepository +@inject ServerService ServerService @inject SmartTranslateService TranslationService
@@ -28,7 +30,8 @@ OnClick="Save" Text="@(TranslationService.Translate("Change"))" WorkingText="@(TranslationService.Translate("Changing"))" - CssClasses="btn-primary"> + CssClasses="btn-primary"> + @@ -55,9 +58,23 @@ private async Task Save() { CurrentServer.Variables.First(x => x.Key == "J2S").Value = Value ? "1" : "0"; - + ServerRepository.Update(CurrentServer); - + + var details = await ServerService.GetDetails(CurrentServer); + + // For better user experience, we start the j2s server right away when the user enables j2s + if (details.State == "offline") + { + await ServerService.SetPowerState(CurrentServer, PowerSignal.Start); + } + + // For better user experience, we kill the j2s server right away when the user disables j2s and the server is starting + if (details.State == "starting") + { + await ServerService.SetPowerState(CurrentServer, PowerSignal.Kill); + } + await Loader.Reload(); } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Profile/Discord.razor b/Moonlight/Shared/Views/Profile/Discord.razor new file mode 100644 index 00000000..c6236d00 --- /dev/null +++ b/Moonlight/Shared/Views/Profile/Discord.razor @@ -0,0 +1,77 @@ +@page "/profile/discord" + +@using Moonlight.Shared.Components.Navigations +@using Moonlight.App.Database.Entities +@using Moonlight.App.Repositories +@using Moonlight.App.Services + +@inject Repository UserRepository +@inject SmartTranslateService SmartTranslateService + + + +@if (User.DiscordId == 0) +{ +
+
+
+ + +
+

+ Your account is currently not linked to discord +

+ To use features like the discord bot, link your moonlight account with your discord account
+
+
+
+ + +
+} +else +{ +
+
+
+ + +
+

+ Your account is linked to a discord account +

+ You are able to use features like the discord bot of moonlight +
+
+
+
+
+ + +
+
+
+} + +@code +{ + [CascadingParameter] + public User User { get; set; } + + private async Task RemoveLink() + { + User.DiscordId = 0; + UserRepository.Update(User); + + await InvokeAsync(StateHasChanged); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Profile/Security.razor b/Moonlight/Shared/Views/Profile/Security.razor index 7cd7bff4..5aad28bc 100644 --- a/Moonlight/Shared/Views/Profile/Security.razor +++ b/Moonlight/Shared/Views/Profile/Security.razor @@ -19,7 +19,7 @@ @inject AlertService AlertService @inject ToastService ToastService - +
diff --git a/Moonlight/Shared/Views/Profile/Subscriptions.razor b/Moonlight/Shared/Views/Profile/Subscriptions.razor index c8382775..2cfc7d1a 100644 --- a/Moonlight/Shared/Views/Profile/Subscriptions.razor +++ b/Moonlight/Shared/Views/Profile/Subscriptions.razor @@ -11,7 +11,7 @@ @inject SubscriptionService SubscriptionService @inject SmartTranslateService SmartTranslateService - +
diff --git a/Moonlight/Shared/Views/Server/Index.razor b/Moonlight/Shared/Views/Server/Index.razor index 63754a15..137f6241 100644 --- a/Moonlight/Shared/Views/Server/Index.razor +++ b/Moonlight/Shared/Views/Server/Index.razor @@ -1,13 +1,13 @@ @page "/server/{ServerUuid}/{Route?}" -@using PteroConsole.NET @using Task = System.Threading.Tasks.Task @using Moonlight.App.Repositories.Servers -@using PteroConsole.NET.Enums @using Microsoft.EntityFrameworkCore @using Logging.Net @using Moonlight.App.Database.Entities @using Moonlight.App.Events @using Moonlight.App.Helpers +@using Moonlight.App.Helpers.Wings +@using Moonlight.App.Helpers.Wings.Enums @using Moonlight.App.Repositories @using Moonlight.App.Services @using Moonlight.Shared.Components.Xterm @@ -44,7 +44,7 @@ { if (NodeOnline) { - if (Console.ConnectionState == ConnectionState.Connected) + if (Console.ConsoleState == ConsoleState.Connected) { if (Console.ServerState == ServerState.Installing) { @@ -179,7 +179,7 @@ [Parameter] public string? Route { get; set; } - private PteroConsole? Console; + private WingsConsole? Console; private Server? CurrentServer; private Node Node; private bool NodeOnline = false; @@ -193,11 +193,11 @@ { Console = new(); - Console.OnConnectionStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; - Console.OnServerResourceUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; - Console.OnServerStateUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; + Console.OnConsoleStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; + Console.OnResourceUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; + Console.OnServerStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; - Console.RequestToken += (_) => WingsConsoleHelper.GenerateToken(CurrentServer!); + Console.OnRequestNewToken += async _ => await WingsConsoleHelper.GenerateToken(CurrentServer!); Console.OnMessage += async (_, s) => { @@ -205,7 +205,10 @@ { if (InstallConsole != null) { - await InstallConsole.WriteLine(s); + if (s.IsInternal) + await InstallConsole.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + s.Content + "\x1b[0m"); + else + await InstallConsole.WriteLine(s.Content); } } }; @@ -280,7 +283,7 @@ await lazyLoader.SetText("Connecting to console"); - await WingsConsoleHelper.ConnectWings(Console!, CurrentServer); + await ReconnectConsole(); await Event.On($"server.{CurrentServer.Uuid}.installComplete", this, server => { @@ -295,6 +298,12 @@ Logger.Debug("Server is null"); } } + + private async Task ReconnectConsole() + { + await Console!.Disconnect(); + await WingsConsoleHelper.ConnectWings(Console!, CurrentServer!); + } public async void Dispose() { @@ -302,5 +311,10 @@ { await Event.Off($"server.{CurrentServer.Uuid}.installComplete", this); } + + if (Console != null) + { + Console.Dispose(); + } } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Servers/Index.razor b/Moonlight/Shared/Views/Servers/Index.razor index b0131db7..dcc2405d 100644 --- a/Moonlight/Shared/Views/Servers/Index.razor +++ b/Moonlight/Shared/Views/Servers/Index.razor @@ -1,5 +1,4 @@ @page "/servers" -@using Moonlight.App.Services.Sessions @using Moonlight.App.Repositories.Servers @using Microsoft.EntityFrameworkCore @using Moonlight.App.Database.Entities @@ -9,9 +8,97 @@ @inject IServiceScopeFactory ServiceScopeFactory - @if (AllServers.Any()) +@if (AllServers.Any()) +{ + if (UseSortedServerView) { - @foreach (var server in AllServers) + var groupedServers = AllServers + .OrderBy(x => x.Name) + .GroupBy(x => x.Image.Name); + + foreach (var groupedServer in groupedServers) + { +
@(groupedServer.Key)
+
+ @foreach (var server in groupedServer) + { +
+ +
+
+
+
+ +
+
+ + @(server.Name) + + + @(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) - @(server.Image.Name) + +
+
+
+
+ @(server.Node.Fqdn):@(server.MainAllocation.Port) +
+
+ @if (StatusCache.ContainsKey(server)) + { + var status = StatusCache[server]; + + switch (status) + { + case "offline": + + Offline + + break; + case "stopping": + + Stopping + + break; + case "starting": + + Starting + + break; + case "running": + + Running + + break; + case "failed": + + Failed + + break; + default: + + Offline + + break; + } + } + else + { + + Loading + + } +
+
+ +
+ } +
+ } + } + else + { + foreach (var server in AllServers) {
@@ -71,29 +172,53 @@
} } - else - { -
-
-

- You have no servers -

- - We were not able to find any servers associated with your account - +} +else +{ +
+
+

+ You have no servers +

+ + We were not able to find any servers associated with your account + +
+
+} + +
+
+
+
+ Beta
- } +
+
+ +
+
+ + +
+
+
+
+
+
@code { [CascadingParameter] public User User { get; set; } - + private Server[] AllServers; private readonly Dictionary StatusCache = new(); + private bool UseSortedServerView = false; + private Task Load(LazyLoader arg) { AllServers = ServerRepository @@ -123,11 +248,11 @@ } }); } - + return Task.CompletedTask; } - private void AddStatus(App.Database.Entities.Server server, string status) + private void AddStatus(Server server, string status) { lock (StatusCache) { diff --git a/Moonlight/defaultstorage/configs/config.json b/Moonlight/defaultstorage/configs/config.json index 19537828..bc1bc396 100644 --- a/Moonlight/defaultstorage/configs/config.json +++ b/Moonlight/defaultstorage/configs/config.json @@ -8,6 +8,10 @@ "Port": "10324", "Username": "user_name" }, + "DiscordBotApi": { + "Enable": false, + "Token": "you api key here" + }, "DiscordBot": { "Enable": false, "Token": "Discord.Token.Here", diff --git a/Moonlight/defaultstorage/resources/lang/de_de.lang b/Moonlight/defaultstorage/resources/lang/de_de.lang index 4f127265..08ed8741 100644 Binary files a/Moonlight/defaultstorage/resources/lang/de_de.lang and b/Moonlight/defaultstorage/resources/lang/de_de.lang differ