72 Commits
v1b ... v1b6

Author SHA1 Message Date
Marcel Baumgartner
c7c39fc511 Merge pull request #167 from Moonlight-Panel/RemoveUselessConsoleLogs
Removed useless console streaming logs
2023-06-13 22:41:46 +02:00
Marcel Baumgartner
3b9bdd1916 Removed useless console streaming logs 2023-06-13 22:35:03 +02:00
Marcel Baumgartner
d267be6d69 Merge pull request #166 from Moonlight-Panel/AddServerFetchBotApi
Added server fetch for single server in bot api
2023-06-13 21:51:20 +02:00
Marcel Baumgartner
18f6a1acdc Added server fetch for single server in bot api 2023-06-13 21:50:33 +02:00
Marcel Baumgartner
2ca41ff18f Merge pull request #165 from Moonlight-Panel/FixSessionListEmailFilter
Fixed session list email filter
2023-06-12 22:12:37 +02:00
Marcel Baumgartner
74c77bc744 Fixed session list email filter 2023-06-12 22:12:05 +02:00
Marcel Baumgartner
1ff8cdd7a9 Merge pull request #164 from Moonlight-Panel/RemovedUnnecessaryReload
Removed unnecessary reload
2023-06-12 00:47:42 +02:00
Marcel Baumgartner
bd320d025a Removed unnecessary reload 2023-06-12 00:47:00 +02:00
Marcel Baumgartner
9a5b004e17 Merge pull request #163 from Moonlight-Panel/AddUserWebsiteDelete
Added webspace delete
2023-06-12 00:10:27 +02:00
Marcel Baumgartner
3aee059860 Added webspace delete 2023-06-12 00:10:03 +02:00
Marcel Baumgartner
3dfa7f66de Merge pull request #162 from Moonlight-Panel/AddUserDomainDelete
Added user domain delete
2023-06-11 22:10:41 +02:00
Marcel Baumgartner
c2949b4773 Added user domain delete 2023-06-11 22:10:28 +02:00
Marcel Baumgartner
c2d0ab4b1b Merge pull request #161 from Moonlight-Panel/FixServerDelete
Fixed server delete
2023-06-11 21:58:56 +02:00
Marcel Baumgartner
de02f0bd74 Fixed server delete 2023-06-11 21:50:45 +02:00
Marcel Baumgartner
e280a95619 Merge pull request #160 from Moonlight-Panel/AddVersioningSystem
Removed old app version copy instruction
2023-06-11 21:11:39 +02:00
Marcel Baumgartner
6f06be9cc6 Removed old app version copy instruction 2023-06-11 21:11:09 +02:00
Marcel Baumgartner
08745a83b4 Merge pull request #159 from Moonlight-Panel/AddVersioningSystem
Add new version and changelog system
2023-06-11 21:06:24 +02:00
Marcel Baumgartner
9a262d1396 Add new version and changelog system 2023-06-11 20:59:20 +02:00
Marcel Baumgartner
0a1b93b8fb Merge pull request #158 from Moonlight-Panel/ImproveStatistics
Added better statistics calculation and active user messurement
2023-06-11 17:57:01 +02:00
Marcel Baumgartner
4b638fc5da Added better statistics calculation and active user messurement 2023-06-11 17:56:45 +02:00
Marcel Baumgartner
d8e34ae891 Merge pull request #157 from Moonlight-Panel/FixSftpWebsitePort
Fixed website sftp port
2023-06-11 16:32:48 +02:00
Marcel Baumgartner
311237e49d Fixed website sftp port 2023-06-11 16:32:28 +02:00
Marcel Baumgartner
6591bbc927 Merge pull request #156 from Moonlight-Panel/AddServerBackgroundImage
Add dynamic background images for servers
2023-06-11 16:28:03 +02:00
Marcel Baumgartner
43c5717d19 Added default background and optimized change methods 2023-06-11 16:26:43 +02:00
Marcel Baumgartner
61d547b2ce Add dynamic background images for servers 2023-06-10 00:00:54 +02:00
Marcel Baumgartner
d7fbe54225 Merge pull request #151 from Moonlight-Panel/AddUptimeCounter
Added uptime service
2023-06-09 15:02:14 +02:00
Marcel Baumgartner
d0004e9fff Added uptime service 2023-06-09 15:01:51 +02:00
Marcel Baumgartner
829596a3e7 Merge pull request #150 from Moonlight-Panel/AddHealthChecks
Add health checks
2023-06-09 14:39:23 +02:00
Marcel Baumgartner
fc319f0f73 Added better error handling and daemon health check 2023-06-09 14:38:30 +02:00
Marcel Baumgartner
bd8ba11410 Merge pull request #149 from Moonlight-Panel/main
Update AddHealthChecks with latest commits
2023-06-09 14:21:28 +02:00
Marcel Baumgartner
0c4fc942b0 Merge pull request #148 from Moonlight-Panel/AddNewDaemonCommunication
Add new daemon communication
2023-06-07 03:34:26 +02:00
Marcel Baumgartner
94b8f07d92 Did some testing. Now able to finish new daemon communication 2023-06-07 03:29:36 +02:00
Marcel Baumgartner
f11eef2734 Merge pull request #147 from Moonlight-Panel/main
Update AddNewDaemonCommunication with latest commits
2023-06-07 02:49:53 +02:00
Marcel Baumgartner
0f8946fe27 Switched to new daemon communication 2023-06-07 02:46:26 +02:00
Marcel Baumgartner
a8cb1392e8 Merge pull request #145 from Moonlight-Panel/ImproveUserExperienceJ2S
Improved user experience for enabling and disabling join2start
2023-06-07 02:39:00 +02:00
Marcel Baumgartner
4241debc3b Improved user experience for enabling and disabling join2start 2023-06-07 02:38:21 +02:00
Marcel Baumgartner
a99959bd2b Merge pull request #144 from Moonlight-Panel/AddDeployNodeOverride
Added smart deploy node override option
2023-06-07 02:23:56 +02:00
Marcel Baumgartner
23644eb93f Added smart deploy node override option 2023-06-07 02:23:30 +02:00
Marcel Baumgartner
f8fcb86ad8 Added base health check and diagnostic system 2023-06-06 22:50:33 +02:00
Marcel Baumgartner
ce0016fa3f Merge pull request #143 from Moonlight-Panel/AddFolderDownloadHandler
Added error handler for folder download
2023-06-05 22:01:40 +02:00
Marcel Baumgartner
15d8f49ce9 Added error handler for folder download 2023-06-05 22:01:21 +02:00
Marcel Baumgartner
98d8e5b755 Merge pull request #142 from Moonlight-Panel/ImproveCpuUsageCalculation
Improved cpu usage calculation
2023-06-05 21:48:15 +02:00
Marcel Baumgartner
bfa1a09aab Improved cpu usage calculation 2023-06-05 21:47:33 +02:00
Marcel Baumgartner
84396c34e6 Merge pull request #140 from Moonlight-Panel/RemoveBundleService
Removed bundle service
2023-06-05 21:34:57 +02:00
Marcel Baumgartner
4fb4a2415b Removed bundle service 2023-06-05 21:34:09 +02:00
Marcel Baumgartner
c6cf11626e Merge pull request #139 from Moonlight-Panel/ImproveConsoleStreaming
Improve console streaming
2023-06-04 21:42:13 +02:00
Marcel Baumgartner
233c304b3c Fixed error when closing a failed websocket connection 2023-06-04 21:41:15 +02:00
Marcel Baumgartner
343e527fb6 Added message cache clear for console streaming 2023-06-04 20:56:47 +02:00
Daniel Balk
25da3c233e Merge pull request #138 from Dannyx1604/main
Ein paar kleine Änderungen ;-)
2023-06-03 12:48:43 +02:00
Dannyx
d7fb3382f7 Ein paar kleine Änderungen ;-) 2023-06-03 09:06:22 +02:00
Marcel Baumgartner
88c9f5372d Merge pull request #137 from Moonlight-Panel/AddConsoleStreamingDispose
Add console streaming dispose
2023-06-01 00:44:56 +02:00
Marcel Baumgartner
7128a7f8a7 Add console streaming dispose 2023-06-01 00:44:14 +02:00
Marcel Baumgartner
6e4f1c1dbd Merge pull request #135 from Moonlight-Panel/FixUnarchiveCrash
Fixed decompress issue (hopefully)
2023-05-28 04:33:30 +02:00
Marcel Baumgartner
3527bc1bd5 Fixed decompress issue (hopefully) 2023-05-28 04:33:11 +02:00
Marcel Baumgartner
c0068b58d7 Merge pull request #134 from Moonlight-Panel/AddNewConsoleStreaming
Added new console streaming
2023-05-28 04:27:26 +02:00
Marcel Baumgartner
feec9426b9 Added new console streaming 2023-05-28 04:27:00 +02:00
Marcel Baumgartner
a180cfa31d Merge pull request #133 from Moonlight-Panel/PatchEventSystem
Patched event system
2023-05-28 03:40:43 +02:00
Marcel Baumgartner
b270b48ac1 Patched event system. Storage issues when using the support chat should be fixed 2023-05-28 03:39:36 +02:00
Marcel Baumgartner
a92c34b47f Merge pull request #132 from Moonlight-Panel/AddDiscordLinkSettings
Implemented new discord linking system
2023-05-26 17:14:28 +02:00
Marcel Baumgartner
ac3bdba3e8 Implemented new discord linking system 2023-05-26 17:14:06 +02:00
Marcel Baumgartner
93328b8b88 Merge pull request #131 from Moonlight-Panel/UIFixes
Added new beta server list ui and added days to uptime formatter
2023-05-26 15:21:16 +02:00
Marcel Baumgartner
800f9fbb50 Added new beta server list ui and added days to uptime formatter 2023-05-26 15:18:59 +02:00
Marcel Baumgartner
ca05f105cf Merge pull request #130 from Moonlight-Panel/ExternalDiscordBotApi
Added external discord bot api
2023-05-23 21:03:27 +02:00
Marcel Baumgartner
cd4d278ceb Added external discord bot api 2023-05-23 21:03:09 +02:00
Marcel Baumgartner
732fbdd46a Merge pull request #129 from Moonlight-Panel/AddBundleService
Add bundle service
2023-05-23 03:30:38 +02:00
Marcel Baumgartner
a308a067d4 Reworked all css and js imports using new bundler 2023-05-23 03:29:52 +02:00
Marcel Baumgartner
f33b218b17 Added base bundle service 2023-05-23 01:32:49 +02:00
Marcel Baumgartner
6a58275681 Merge pull request #128 from Moonlight-Panel/FixIpBanUpdate
Reload instead of rerender sessions when ip banned
2023-05-22 23:18:39 +02:00
Marcel Baumgartner
eeba837009 Reload instead of rerender sessions when ip banned 2023-05-22 23:18:09 +02:00
Marcel Baumgartner
8098368660 Hopefully fixed errors when building in github actions 2023-05-22 09:46:23 +02:00
Marcel Baumgartner
863ac22036 Update test-docker-build.yml 2023-05-22 09:42:21 +02:00
Marcel Baumgartner
718342d532 Added new github action to find compile errors on commits to main 2023-05-22 09:39:26 +02:00
97 changed files with 4777 additions and 1045 deletions

18
.github/workflows/test-docker-build.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Test Build
on:
push:
branches: [ "main" ]
pull_request:
types:
- closed
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t moonlightpanel/moonlight:${{ github.sha }} -f Moonlight/Dockerfile .

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ Desktop.ini
storage/ storage/
Moonlight/publish.ps1 Moonlight/publish.ps1
Moonlight/version

View File

@@ -1,5 +1,4 @@
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Newtonsoft.Json; using Newtonsoft.Json;
using RestSharp; using RestSharp;
@@ -14,26 +13,13 @@ public class DaemonApiHelper
Client = new(); Client = new();
} }
private string GetApiUrl(Node node)
{
/* SSL not implemented in moonlight daemon
if(node.Ssl)
return $"https://{node.Fqdn}:{node.MoonlightDaemonPort}/";
else
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";*/
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
}
public async Task<T> Get<T>(Node node, string resource) public async Task<T> Get<T>(Node node, string resource)
{ {
RestRequest request = new(GetApiUrl(node) + resource); var request = await CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json"); request.Method = Method.Get;
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
var response = await Client.GetAsync(request); var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful) if (!response.IsSuccessful)
{ {
@@ -52,4 +38,69 @@ public class DaemonApiHelper
return JsonConvert.DeserializeObject<T>(response.Content!)!; return JsonConvert.DeserializeObject<T>(response.Content!)!;
} }
public async Task Post(Node node, string resource, object body)
{
var request = await CreateRequest(node, resource);
request.Method = Method.Post;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new DaemonException(
$"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)
{
var request = await CreateRequest(node, resource);
request.Method = Method.Delete;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new DaemonException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
}
private Task<RestRequest> CreateRequest(Node node, string resource)
{
var url = $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
RestRequest request = new(url + resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
return Task.FromResult(request);
}
} }

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.ApiClients.Daemon.Requests;
public class Mount
{
public string Server { get; set; } = "";
public string ServerPath { get; set; } = "";
public string Path { get; set; } = "";
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.ApiClients.Daemon.Requests;
public class Unmount
{
public string Path { get; set; } = "";
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class Container
{
public string Name { get; set; } = "";
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
}

View File

@@ -1,15 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class ContainerStats
{
public List<Container> Containers { get; set; } = new();
public class Container
{
public string Name { get; set; }
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class CpuMetrics
{
public string CpuModel { get; set; } = "";
public double CpuUsage { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class CpuStats
{
public double Usage { get; set; }
public int Cores { get; set; }
public string Model { get; set; } = "";
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DiskMetrics
{
public long Used { get; set; }
public long Total { get; set; }
}

View File

@@ -1,9 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DiskStats
{
public long FreeBytes { get; set; }
public string DriveFormat { get; set; }
public string Name { get; set; }
public long TotalSize { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DockerMetrics
{
public Container[] Containers { get; set; } = Array.Empty<Container>();
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class MemoryMetrics
{
public long Used { get; set; }
public long Total { get; set; }
}

View File

@@ -1,15 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class MemoryStats
{
public List<MemoryStick> Sticks { get; set; } = new();
public double Free { get; set; }
public double Used { get; set; }
public double Total { get; set; }
public class MemoryStick
{
public int Size { get; set; }
public string Type { get; set; } = "";
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class SystemMetrics
{
public string OsName { get; set; } = "";
public long Uptime { get; set; }
}

View File

@@ -20,4 +20,5 @@ public class Image
public List<DockerImage> DockerImages { get; set; } = new(); public List<DockerImage> DockerImages { get; set; } = new();
public List<ImageVariable> Variables { get; set; } = new(); public List<ImageVariable> Variables { get; set; } = new();
public string TagsJson { get; set; } = ""; public string TagsJson { get; set; } = "";
public string BackgroundImageUrl { get; set; } = "";
} }

View File

@@ -43,6 +43,7 @@ public class User
// Date stuff // Date stuff
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastVisitedAt { get; set; } = DateTime.UtcNow;
// Subscriptions // Subscriptions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddBackgroundImageUrlImage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BackgroundImageUrl",
table: "Images",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BackgroundImageUrl",
table: "Images");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddLastVisitedTimestamp : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastVisitedAt",
table: "Users",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastVisitedAt",
table: "Users");
}
}
}

View File

@@ -132,6 +132,10 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Allocations") b.Property<int>("Allocations")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("BackgroundImageUrl")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ConfigFiles") b.Property<string>("ConfigFiles")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
@@ -758,6 +762,9 @@ namespace Moonlight.App.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<DateTime>("LastVisitedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Password") b.Property<string>("Password")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");

View File

@@ -0,0 +1,58 @@
using System.Diagnostics;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class DaemonHealthCheck : IHealthCheck
{
private readonly Repository<Node> NodeRepository;
private readonly NodeService NodeService;
public DaemonHealthCheck(Repository<Node> nodeRepository, NodeService nodeService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
var nodes = NodeRepository.Get().ToArray();
var results = new Dictionary<Node, bool>();
var healthCheckData = new Dictionary<string, object>();
foreach (var node in nodes)
{
try
{
await NodeService.GetCpuMetrics(node);
results.Add(node, true);
}
catch (Exception e)
{
results.Add(node, false);
healthCheckData.Add(node.Name, e.ToStringDemystified());
}
}
var offlineNodes = results
.Where(x => !x.Value)
.ToArray();
if (offlineNodes.Length == nodes.Length)
{
return HealthCheckResult.Unhealthy("All node daemons are offline", null, healthCheckData);
}
if (offlineNodes.Length == 0)
{
return HealthCheckResult.Healthy("All node daemons are online");
}
return HealthCheckResult.Degraded($"{offlineNodes.Length} node daemons are offline", null, healthCheckData);
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class DatabaseHealthCheck : IHealthCheck
{
private readonly DataContext DataContext;
public DatabaseHealthCheck(DataContext dataContext)
{
DataContext = dataContext;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = new CancellationToken())
{
try
{
await DataContext.Database.OpenConnectionAsync(cancellationToken);
await DataContext.Database.CloseConnectionAsync();
return HealthCheckResult.Healthy("Database is online");
}
catch (Exception e)
{
return HealthCheckResult.Unhealthy("Database is offline", e);
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Diagnostics;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class NodeHealthCheck : IHealthCheck
{
private readonly Repository<Node> NodeRepository;
private readonly NodeService NodeService;
public NodeHealthCheck(Repository<Node> nodeRepository, NodeService nodeService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
var nodes = NodeRepository.Get().ToArray();
var results = new Dictionary<Node, bool>();
var healthCheckData = new Dictionary<string, object>();
foreach (var node in nodes)
{
try
{
await NodeService.GetStatus(node);
results.Add(node, true);
}
catch (Exception e)
{
results.Add(node, false);
healthCheckData.Add(node.Name, e.ToStringDemystified());
}
}
var offlineNodes = results
.Where(x => !x.Value)
.ToArray();
if (offlineNodes.Length == nodes.Length)
{
return HealthCheckResult.Unhealthy("All nodes are offline", null, healthCheckData);
}
if (offlineNodes.Length == 0)
{
return HealthCheckResult.Healthy("All nodes are online");
}
return HealthCheckResult.Degraded($"{offlineNodes.Length} nodes are offline", null, healthCheckData);
}
}

View File

@@ -5,7 +5,6 @@ namespace Moonlight.App.Events;
public class EventSystem public class EventSystem
{ {
private Dictionary<int, object> Storage = new();
private List<Subscriber> Subscribers = new(); private List<Subscriber> Subscribers = new();
private readonly bool Debug = false; private readonly bool Debug = false;
@@ -33,16 +32,8 @@ public class EventSystem
return Task.CompletedTask; 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; Subscriber[] subscribers;
lock (Subscribers) lock (Subscribers)
@@ -58,23 +49,6 @@ public class EventSystem
{ {
tasks.Add(new Task(() => 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(); var stopWatch = new Stopwatch();
stopWatch.Start(); stopWatch.Start();
@@ -115,7 +89,6 @@ public class EventSystem
Task.Run(() => Task.Run(() =>
{ {
Task.WaitAll(tasks.ToArray()); Task.WaitAll(tasks.ToArray());
Storage.Remove(hashCode);
if(Debug) if(Debug)
Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage"); Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage");

View File

@@ -0,0 +1,39 @@
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Helpers;
public static class AvgHelper
{
public static StatisticsData[] Calculate(StatisticsData[] data, int splitSize = 40)
{
if (data.Length <= splitSize)
return data;
var result = new List<StatisticsData>();
var i = data.Length / (float)splitSize;
var pc = (int)Math.Round(i);
foreach (var part in data.Chunk(pc))
{
double d = 0;
var res = new StatisticsData();
foreach (var entry in part)
{
d += entry.Value;
}
res.Chart = part.First().Chart;
res.Date = part.First().Date;
if (d == 0)
res.Value = 0;
res.Value = d / part.Length;
result.Add(res);
}
return result.ToArray();
}
}

View File

@@ -8,16 +8,22 @@ using MySql.Data.MySqlClient;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers;
public class DatabaseCheckup public class DatabaseCheckupService
{ {
public static void Perform() private readonly ConfigService ConfigService;
public DatabaseCheckupService(ConfigService configService)
{ {
// This will also copy all default config files ConfigService = configService;
var context = new DataContext(new ConfigService(new StorageService())); }
public async Task Perform()
{
var context = new DataContext(ConfigService);
Logger.Info("Checking database"); Logger.Info("Checking database");
if (!context.Database.CanConnect()) if (!await context.Database.CanConnectAsync())
{ {
Logger.Fatal("-----------------------------------------------"); Logger.Fatal("-----------------------------------------------");
Logger.Fatal("Unable to connect to mysql database"); Logger.Fatal("Unable to connect to mysql database");
@@ -32,19 +38,19 @@ public class DatabaseCheckup
Logger.Info("Checking for pending migrations"); Logger.Info("Checking for pending migrations");
var migrations = context.Database var migrations = (await context.Database
.GetPendingMigrations() .GetPendingMigrationsAsync())
.ToArray(); .ToArray();
if (migrations.Any()) if (migrations.Any())
{ {
Logger.Info($"{migrations.Length} migrations pending. Updating now"); Logger.Info($"{migrations.Length} migrations pending. Updating now");
BackupDatabase(); await BackupDatabase();
Logger.Info("Applying migrations"); Logger.Info("Applying migrations");
context.Database.Migrate(); await context.Database.MigrateAsync();
Logger.Info("Successfully applied migrations"); Logger.Info("Successfully applied migrations");
} }
@@ -54,7 +60,7 @@ public class DatabaseCheckup
} }
} }
public static void BackupDatabase() public async Task BackupDatabase()
{ {
Logger.Info("Creating backup from database"); Logger.Info("Creating backup from database");
@@ -79,14 +85,14 @@ public class DatabaseCheckup
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
using MySqlConnection conn = new MySqlConnection(connectionString); await using MySqlConnection conn = new MySqlConnection(connectionString);
using MySqlCommand cmd = new MySqlCommand(); await using MySqlCommand cmd = new MySqlCommand();
using MySqlBackup mb = new MySqlBackup(cmd); using MySqlBackup mb = new MySqlBackup(cmd);
cmd.Connection = conn; cmd.Connection = conn;
conn.Open(); await conn.OpenAsync();
mb.ExportToFile(file); mb.ExportToFile(file);
conn.Close(); await conn.CloseAsync();
sw.Stop(); sw.Stop();
Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s"); Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s");

View File

@@ -3,6 +3,7 @@ using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Requests; using Moonlight.App.ApiClients.Wings.Requests;
using Moonlight.App.ApiClients.Wings.Resources; using Moonlight.App.ApiClients.Wings.Resources;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Services; using Moonlight.App.Services;
using RestSharp; using RestSharp;
@@ -210,6 +211,8 @@ public class WingsFileAccess : FileAccess
} }
public override async Task Decompress(FileData fileData) public override async Task Decompress(FileData fileData)
{
try
{ {
var req = new DecompressFile() var req = new DecompressFile()
{ {
@@ -219,6 +222,18 @@ public class WingsFileAccess : FileAccess
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<string> GetLaunchUrl() public override Task<string> GetLaunchUrl()
{ {

View File

@@ -8,8 +8,27 @@ public static class Formatter
{ {
TimeSpan t = TimeSpan.FromMilliseconds(uptime); TimeSpan t = TimeSpan.FromMilliseconds(uptime);
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"; return $"{t.Hours}h {t.Minutes}m {t.Seconds}s";
} }
}
public static string FormatUptime(TimeSpan t)
{
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) private static double Round(this double d, int decimals)
{ {
@@ -109,4 +128,12 @@ public static class Formatter
return (i / (1024D * 1024D)).Round(2) + " GB"; return (i / (1024D * 1024D)).Round(2) + " GB";
} }
} }
public static double BytesToGb(long bytes)
{
const double gbMultiplier = 1024 * 1024 * 1024; // 1 GB = 1024 MB * 1024 KB * 1024 B
double gigabytes = (double)bytes / gbMultiplier;
return gigabytes;
}
} }

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Helpers.Wings.Data;
public class ConsoleMessage
{
public string Content { get; set; } = "";
public bool IsInternal { get; set; } = false;
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.Helpers.Wings.Enums;
public enum ConsoleState
{
Disconnected,
Connecting,
Connected
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.App.Helpers.Wings.Enums;
public enum ServerState
{
Starting,
Running,
Stopping,
Offline,
Installing
}

View File

@@ -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<string>();
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Helpers.Wings.Events;
public class SendTokenEvent
{
public string Event { get; set; } = "auth";
public List<string> Args = new();
}

View File

@@ -0,0 +1,393 @@
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<ConsoleMessage> 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<ConsoleState> OnConsoleStateUpdated { get; set; }
public EventHandler<ServerState> OnServerStateUpdated { get; set; }
public EventHandler<ServerResource> OnResourceUpdated { get; set; }
public EventHandler<ConsoleMessage> OnMessage { get; set; }
public Func<WingsConsole, Task<string>> 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 (JsonReaderException)
{
// ignore
}
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<BaseEvent>(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<ServerResource>(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<byte>(byteContentBuffer), WebSocketMessageType.Text, true,
CancellationToken.None);
}
}
private async Task<string> ReceiveRaw()
{
ArraySegment<byte> receivedBytes = new ArraySegment<byte>(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();
}
}

View File

@@ -7,37 +7,34 @@ using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Moonlight.App.Services; using Moonlight.App.Services;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers.Wings;
public class WingsConsoleHelper public class WingsConsoleHelper
{ {
private readonly ServerRepository ServerRepository; private readonly ServerRepository ServerRepository;
private readonly WingsJwtHelper WingsJwtHelper;
private readonly string AppUrl; private readonly string AppUrl;
public WingsConsoleHelper( public WingsConsoleHelper(
ServerRepository serverRepository, ServerRepository serverRepository,
ConfigService configService, ConfigService configService)
WingsJwtHelper wingsJwtHelper)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
WingsJwtHelper = wingsJwtHelper;
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl"); AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
} }
public async Task ConnectWings(PteroConsole.NET.PteroConsole pteroConsole, Server server) public async Task ConnectWings(WingsConsole console, Server server)
{ {
var serverData = ServerRepository var serverData = ServerRepository
.Get() .Get()
.Include(x => x.Node) .Include(x => x.Node)
.First(x => x.Id == server.Id); .First(x => x.Id == server.Id);
var token = GenerateToken(serverData); var token = await GenerateToken(serverData);
if (serverData.Node.Ssl) if (serverData.Node.Ssl)
{ {
await pteroConsole.Connect( await console.Connect(
AppUrl, AppUrl,
$"wss://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws", $"wss://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws",
token token
@@ -45,7 +42,7 @@ public class WingsConsoleHelper
} }
else else
{ {
await pteroConsole.Connect( await console.Connect(
AppUrl, AppUrl,
$"ws://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws", $"ws://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws",
token token
@@ -53,7 +50,7 @@ public class WingsConsoleHelper
} }
} }
public string GenerateToken(Server server) public async Task<string> GenerateToken(Server server)
{ {
var serverData = ServerRepository var serverData = ServerRepository
.Get() .Get()
@@ -66,7 +63,7 @@ public class WingsConsoleHelper
using (MD5 md5 = MD5.Create()) 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 outputBytes = md5.ComputeHash(inputBytes);
var identifier = Convert.ToHexString(outputBytes).ToLower(); var identifier = Convert.ToHexString(outputBytes).ToLower();
@@ -77,7 +74,7 @@ public class WingsConsoleHelper
.WithAlgorithm(new HMACSHA256Algorithm()) .WithAlgorithm(new HMACSHA256Algorithm())
.WithSecret(secret) .WithSecret(secret)
.AddClaim("user_id", userid) .AddClaim("user_id", userid)
.AddClaim("server_uuid", serverData.Uuid.ToString()) .AddClaim("server_uuid", server.Uuid.ToString())
.AddClaim("permissions", new[] .AddClaim("permissions", new[]
{ {
"*", "*",

View File

@@ -4,7 +4,7 @@ using JWT.Algorithms;
using JWT.Builder; using JWT.Builder;
using Moonlight.App.Services; using Moonlight.App.Services;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers.Wings;
public class WingsJwtHelper public class WingsJwtHelper
{ {

View File

@@ -5,7 +5,7 @@ using Moonlight.App.Http.Resources.Wings;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers.Wings;
public class WingsServerConverter public class WingsServerConverter
{ {

View File

@@ -0,0 +1,176 @@
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<User> UserRepository;
private readonly Repository<Server> ServerRepository;
private readonly ServerService ServerService;
private readonly string Token = "";
private readonly bool Enable;
public DiscordBotController(
Repository<User> userRepository,
Repository<Server> serverRepository,
ServerService serverService,
ConfigService configService)
{
UserRepository = userRepository;
ServerRepository = serverRepository;
ServerService = serverService;
var config = configService
.GetSection("Moonlight")
.GetSection("DiscordBotApi");
Enable = config.GetValue<bool>("Enable");
if (Enable)
{
Token = config.GetValue<string>("Token");
}
}
[HttpGet("{id}/link")]
public async Task<ActionResult> 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<ActionResult<Server[]>> 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<ActionResult> 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}/details")]
public async Task<ActionResult<ServerDetails>> 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);
}
[HttpGet("{id}/servers/{uuid}")]
public async Task<ActionResult<ServerDetails>> GetServer(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)
.Include(x => x.Image)
.Include(x => x.Node)
.FirstOrDefault(x => x.Owner.Id == user.Id && x.Uuid == uuid);
if (server == null)
return NotFound();
server.Node.Token = "";
server.Node.TokenId = "";
return Ok(server);
}
private Task<User?> GetUserFromDiscordId(ulong discordId)
{
var user = UserRepository
.Get()
.FirstOrDefault(x => x.DiscordId == discordId);
return Task.FromResult(user);
}
private Task<bool> 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);
}
}

View File

@@ -1,6 +1,7 @@
using Logging.Net; using Logging.Net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Services; using Moonlight.App.Services;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Http.Controllers.Api.Moonlight; namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -11,12 +12,41 @@ public class OAuth2Controller : Controller
private readonly UserService UserService; private readonly UserService UserService;
private readonly OAuth2Service OAuth2Service; private readonly OAuth2Service OAuth2Service;
private readonly DateTimeService DateTimeService; 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; UserService = userService;
OAuth2Service = oAuth2Service; OAuth2Service = oAuth2Service;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
IdentityService = identityService;
}
[HttpGet("{id}/start")]
public async Task<ActionResult> 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}")] [HttpGet("{id}")]
@@ -24,6 +54,18 @@ public class OAuth2Controller : Controller
{ {
try 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); var user = await OAuth2Service.HandleCode(id, code);
Response.Cookies.Append("token", await UserService.GenerateToken(user), new() Response.Cookies.Append("token", await UserService.GenerateToken(user), new()

View File

@@ -1,10 +1,12 @@
using Logging.Net; using System.Text;
using Logging.Net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Services; using Moonlight.App.Services;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;
using Moonlight.App.Services.LogServices; using Moonlight.App.Services.LogServices;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Http.Controllers.Api.Moonlight; namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -45,6 +47,29 @@ public class ResourcesController : Controller
return NotFound(); return NotFound();
} }
[HttpGet("background/{name}")]
public async Task<ActionResult> GetBackground([FromRoute] string name)
{
if (name.Contains(".."))
{
await SecurityLogService.Log(SecurityLogType.PathTransversal, x =>
{
x.Add<string>(name);
});
return NotFound();
}
if (System.IO.File.Exists(PathBuilder.File("storage", "resources", "public", "background", name)))
{
var fs = new FileStream(PathBuilder.File("storage", "resources", "public", "background", name), FileMode.Open);
return File(fs, MimeTypes.GetMimeType(name), name);
}
return NotFound();
}
[HttpGet("bucket/{bucket}/{name}")] [HttpGet("bucket/{bucket}/{name}")]
public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name) public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name)
{ {

View File

@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Http.Resources.Wings; using Moonlight.App.Http.Resources.Wings;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;

View File

@@ -0,0 +1,8 @@
using Moonlight.App.ApiClients.Wings;
namespace Moonlight.App.Http.Requests.DiscordBot.Requests;
public class SetPowerSignal
{
public string Signal { get; set; }
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace Moonlight.App.Models.Misc;
public class HealthCheck
{
public string Status { get; set; }
public TimeSpan TotalDuration { get; set; }
public Dictionary<string, HealthCheckEntry> Entries { get; set; } = new();
public class HealthCheckEntry
{
public Dictionary<string, string> Data { get; set; } = new();
public string Description { get; set; }
public TimeSpan Duration { get; set; }
public string Status { get; set; }
public List<string> Tags { get; set; } = new();
}
}

View File

@@ -6,5 +6,5 @@ namespace Moonlight.App.Models.Misc;
public class RunningServer public class RunningServer
{ {
public Server Server { get; set; } public Server Server { get; set; }
public ContainerStats.Container Container { get; set; } public Container Container { get; set; }
} }

View File

@@ -9,7 +9,9 @@ public abstract class OAuth2Provider
public string Url { get; set; } public string Url { get; set; }
public IServiceScopeFactory ServiceScopeFactory { get; set; } public IServiceScopeFactory ServiceScopeFactory { get; set; }
public string DisplayName { get; set; } public string DisplayName { get; set; }
public bool CanBeLinked { get; set; } = false;
public abstract Task<string> GetUrl(); public abstract Task<string> GetUrl();
public abstract Task<User> HandleCode(string code); public abstract Task<User> HandleCode(string code);
public abstract Task LinkToUser(User user, string code);
} }

View File

@@ -12,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class DiscordOAuth2Provider : OAuth2Provider public class DiscordOAuth2Provider : OAuth2Provider
{ {
public DiscordOAuth2Provider()
{
CanBeLinked = true;
}
public override Task<string> GetUrl() public override Task<string> GetUrl()
{ {
string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" + string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" +
@@ -119,4 +124,74 @@ public class DiscordOAuth2Provider : OAuth2Provider
return user; 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<string>("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<ulong>("id");
// Handle data
using var scope = ServiceScopeFactory.CreateScope();
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
user.DiscordId = id;
userRepo.Update(user);
}
} }

View File

@@ -4,7 +4,6 @@ using Moonlight.App.ApiClients.Google.Requests;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services; using Moonlight.App.Services;
using RestSharp; using RestSharp;
@@ -13,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class GoogleOAuth2Provider : OAuth2Provider public class GoogleOAuth2Provider : OAuth2Provider
{ {
public GoogleOAuth2Provider()
{
CanBeLinked = false;
}
public override Task<string> GetUrl() public override Task<string> GetUrl()
{ {
var endpoint = Url + "/api/moonlight/oauth2/google"; var endpoint = Url + "/api/moonlight/oauth2/google";
@@ -127,4 +131,9 @@ public class GoogleOAuth2Provider : OAuth2Provider
return user; return user;
} }
} }
public override Task LinkToUser(User user, string code)
{
throw new NotImplementedException();
}
} }

View File

@@ -5,6 +5,7 @@ using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -81,12 +82,12 @@ public class CleanupService
{ {
try try
{ {
var cpuStats = await nodeService.GetCpuStats(node); var cpuMetrics = await nodeService.GetCpuMetrics(node);
var memoryStats = await nodeService.GetMemoryStats(node); var memoryMetrics = await nodeService.GetMemoryMetrics(node);
if (cpuStats.Usage > maxCpu || memoryStats.Free < minMemory) if (cpuMetrics.CpuUsage > maxCpu || (Formatter.BytesToGb(memoryMetrics.Total) - (Formatter.BytesToGb(memoryMetrics.Used))) < minMemory)
{ {
var containerStats = await nodeService.GetContainerStats(node); var dockerMetrics = await nodeService.GetDockerMetrics(node);
var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>(); var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>();
var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>(); var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>();
@@ -101,9 +102,9 @@ public class CleanupService
) )
.ToArray(); .ToArray();
var containerMappedToServers = new Dictionary<ContainerStats.Container, Server>(); var containerMappedToServers = new Dictionary<Container, Server>();
foreach (var container in containerStats.Containers) foreach (var container in dockerMetrics.Containers)
{ {
if (Guid.TryParse(container.Name, out Guid uuid)) if (Guid.TryParse(container.Name, out Guid uuid))
{ {

View File

@@ -16,6 +16,11 @@ public class ResourceService
return $"{AppUrl}/api/moonlight/resources/images/{name}"; return $"{AppUrl}/api/moonlight/resources/images/{name}";
} }
public string BackgroundImage(string name)
{
return $"{AppUrl}/api/moonlight/resources/background/{name}";
}
public string Avatar(User user) public string Avatar(User user)
{ {
return $"{AppUrl}/api/moonlight/avatar/{user.Id}"; return $"{AppUrl}/api/moonlight/avatar/{user.Id}";

View File

@@ -0,0 +1,101 @@
using Logging.Net;
using Octokit;
using Repository = LibGit2Sharp.Repository;
namespace Moonlight.App.Services;
public class MoonlightService
{
private readonly ConfigService ConfigService;
public readonly DateTime StartTimestamp;
public readonly string AppVersion;
public readonly List<string[]> ChangeLog = new();
public MoonlightService(ConfigService configService)
{
ConfigService = configService;
StartTimestamp = DateTime.UtcNow;
if (File.Exists("version") && !ConfigService.DebugMode)
AppVersion = File.ReadAllText("version");
else if (ConfigService.DebugMode)
{
string repositoryPath = Path.GetFullPath("..");
using var repo = new Repository(repositoryPath);
var commit = repo.Head.Tip;
AppVersion = commit.Sha;
}
else
AppVersion = "unknown";
Task.Run(FetchChangeLog);
}
private async Task FetchChangeLog()
{
if(AppVersion == "unknown")
return;
if (ConfigService.DebugMode)
{
ChangeLog.Add(new[]
{
"Disabled",
"Fetching changelog from github is disabled in debug mode"
});
return;
}
try
{
var client = new GitHubClient(new ProductHeaderValue("Moonlight"));
var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest
{
State = ItemStateFilter.Closed,
SortDirection = SortDirection.Ascending,
SortProperty = PullRequestSort.Created
});
var groupedPullRequests = new Dictionary<DateTime, List<string>>();
foreach (var pullRequest in pullRequests)
{
if (pullRequest.MergedAt != null)
{
var date = pullRequest.MergedAt.Value.Date;
if (!groupedPullRequests.ContainsKey(date))
{
groupedPullRequests[date] = new List<string>();
}
groupedPullRequests[date].Add(pullRequest.Title);
}
}
int i = 1;
foreach (var group in groupedPullRequests)
{
var pullRequestsList = new List<string>();
var date = group.Key.ToString("dd.MM.yyyy");
pullRequestsList.Add($"Patch {i}, {date}");
foreach (var pullRequest in group.Value)
{
pullRequestsList.Add(pullRequest);
}
ChangeLog.Add(pullRequestsList.ToArray());
i++;
}
}
catch (Exception e)
{
Logger.Warn("Error fetching changelog");
Logger.Warn(e);
}
}
}

View File

@@ -1,4 +1,5 @@
using Moonlight.App.ApiClients.Daemon; using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Daemon.Requests;
using Moonlight.App.ApiClients.Daemon.Resources; using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Resources; using Moonlight.App.ApiClients.Wings.Resources;
@@ -24,34 +25,55 @@ public class NodeService
return await WingsApiHelper.Get<SystemStatus>(node, "api/system"); return await WingsApiHelper.Get<SystemStatus>(node, "api/system");
} }
public async Task<CpuStats> GetCpuStats(Node node) public async Task<CpuMetrics> GetCpuMetrics(Node node)
{ {
return await DaemonApiHelper.Get<CpuStats>(node, "stats/cpu"); return await DaemonApiHelper.Get<CpuMetrics>(node, "metrics/cpu");
} }
public async Task<MemoryStats> GetMemoryStats(Node node) public async Task<MemoryMetrics> GetMemoryMetrics(Node node)
{ {
return await DaemonApiHelper.Get<MemoryStats>(node, "stats/memory"); return await DaemonApiHelper.Get<MemoryMetrics>(node, "metrics/memory");
} }
public async Task<DiskStats> GetDiskStats(Node node) public async Task<DiskMetrics> GetDiskMetrics(Node node)
{ {
return await DaemonApiHelper.Get<DiskStats>(node, "stats/disk"); return await DaemonApiHelper.Get<DiskMetrics>(node, "metrics/disk");
} }
public async Task<ContainerStats> GetContainerStats(Node node) public async Task<SystemMetrics> GetSystemMetrics(Node node)
{ {
return await DaemonApiHelper.Get<ContainerStats>(node, "stats/container"); return await DaemonApiHelper.Get<SystemMetrics>(node, "metrics/system");
}
public async Task<DockerMetrics> GetDockerMetrics(Node node)
{
return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker");
}
public async Task Mount(Node node, string server, string serverPath, string path)
{
await DaemonApiHelper.Post(node, "mount", new Mount()
{
Server = server,
ServerPath = serverPath,
Path = path
});
}
public async Task Unmount(Node node, string path)
{
await DaemonApiHelper.Delete(node, "mount", new Unmount()
{
Path = path
});
} }
public async Task<bool> IsHostUp(Node node) public async Task<bool> IsHostUp(Node node)
{ {
try try
{ {
//TODO: Implement status caching await GetSystemMetrics(node);
var data = await GetStatus(node);
if (data != null)
return true; return true;
} }
catch (Exception) catch (Exception)

View File

@@ -80,6 +80,26 @@ public class OAuth2Service
return await provider.HandleCode(code); return await provider.HandleCode(code);
} }
public Task<bool> 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() private string GetAppUrl()
{ {
if (EnableOverrideUrl) if (EnableOverrideUrl)

View File

@@ -8,6 +8,7 @@ using Moonlight.App.Events;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Files; using Moonlight.App.Helpers.Files;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
@@ -18,6 +19,7 @@ namespace Moonlight.App.Services;
public class ServerService public class ServerService
{ {
private readonly Repository<ServerVariable> ServerVariablesRepository;
private readonly ServerRepository ServerRepository; private readonly ServerRepository ServerRepository;
private readonly UserRepository UserRepository; private readonly UserRepository UserRepository;
private readonly ImageRepository ImageRepository; private readonly ImageRepository ImageRepository;
@@ -49,7 +51,8 @@ public class ServerService
NodeService nodeService, NodeService nodeService,
NodeAllocationRepository nodeAllocationRepository, NodeAllocationRepository nodeAllocationRepository,
DateTimeService dateTimeService, DateTimeService dateTimeService,
EventSystem eventSystem) EventSystem eventSystem,
Repository<ServerVariable> serverVariablesRepository)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
WingsApiHelper = wingsApiHelper; WingsApiHelper = wingsApiHelper;
@@ -66,6 +69,7 @@ public class ServerService
NodeAllocationRepository = nodeAllocationRepository; NodeAllocationRepository = nodeAllocationRepository;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
Event = eventSystem; Event = eventSystem;
ServerVariablesRepository = serverVariablesRepository;
} }
private Server EnsureNodeData(Server s) private Server EnsureNodeData(Server s)
@@ -400,17 +404,13 @@ public class ServerService
public async Task Delete(Server s) public async Task Delete(Server s)
{ {
throw new DisplayException("Deleting a server is currently a bit buggy. So its disabled for your safety"); var backups = await GetBackups(s);
var server = EnsureNodeData(s);
var backups = await GetBackups(server);
foreach (var backup in backups) foreach (var backup in backups)
{ {
try try
{ {
await DeleteBackup(server, backup); await DeleteBackup(s, backup);
} }
catch (Exception) catch (Exception)
{ {
@@ -418,9 +418,18 @@ public class ServerService
} }
} }
var server = ServerRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.Node)
.First(x => x.Id == s.Id);
await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null); await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null);
//TODO: Fix empty data models foreach (var variable in server.Variables.ToArray())
{
ServerVariablesRepository.Delete(variable);
}
server.Allocations = new(); server.Allocations = new();
server.MainAllocation = null; server.MainAllocation = null;
@@ -428,7 +437,6 @@ public class ServerService
server.Backups = new(); server.Backups = new();
ServerRepository.Update(server); ServerRepository.Update(server);
ServerRepository.Delete(server); ServerRepository.Delete(server);
} }

View File

@@ -0,0 +1,39 @@
using Logging.Net;
using Moonlight.App.Services.Files;
namespace Moonlight.App.Services.Sessions;
public class DynamicBackgroundService
{
public EventHandler OnBackgroundImageChanged { get; set; }
public string BackgroundImageUrl { get; private set; }
private string DefaultBackgroundImageUrl;
public DynamicBackgroundService(ResourceService resourceService)
{
DefaultBackgroundImageUrl = resourceService.BackgroundImage("main.jpg");
BackgroundImageUrl = DefaultBackgroundImageUrl;
}
public Task Change(string url)
{
if(BackgroundImageUrl == url) // Prevent unnecessary updates
return Task.CompletedTask;
BackgroundImageUrl = url;
OnBackgroundImageChanged?.Invoke(this, null!);
return Task.CompletedTask;
}
public Task Reset()
{
if(BackgroundImageUrl == DefaultBackgroundImageUrl) // Prevent unnecessary updates
return Task.CompletedTask;
BackgroundImageUrl = DefaultBackgroundImageUrl;
OnBackgroundImageChanged?.Invoke(this, null!);
return Task.CompletedTask;
}
}

View File

@@ -9,6 +9,7 @@ namespace Moonlight.App.Services.Sessions;
public class SessionService public class SessionService
{ {
private readonly SessionRepository SessionRepository; private readonly SessionRepository SessionRepository;
private Repository<User> UserRepository;
private readonly IdentityService IdentityService; private readonly IdentityService IdentityService;
private readonly NavigationManager NavigationManager; private readonly NavigationManager NavigationManager;
private readonly AlertService AlertService; private readonly AlertService AlertService;
@@ -21,13 +22,15 @@ public class SessionService
IdentityService identityService, IdentityService identityService,
NavigationManager navigationManager, NavigationManager navigationManager,
AlertService alertService, AlertService alertService,
DateTimeService dateTimeService) DateTimeService dateTimeService,
Repository<User> userRepository)
{ {
SessionRepository = sessionRepository; SessionRepository = sessionRepository;
IdentityService = identityService; IdentityService = identityService;
NavigationManager = navigationManager; NavigationManager = navigationManager;
AlertService = alertService; AlertService = alertService;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
UserRepository = userRepository;
} }
public async Task Register() public async Task Register()
@@ -46,6 +49,12 @@ public class SessionService
}; };
SessionRepository.Add(OwnSession); SessionRepository.Add(OwnSession);
if (user != null) // Track last session init of user as last visited timestamp
{
user.LastVisitedAt = DateTimeService.GetCurrent();
UserRepository.Update(user);
}
} }
public void Refresh() public void Refresh()

View File

@@ -9,21 +9,36 @@ public class SmartDeployService
private readonly Repository<CloudPanel> CloudPanelRepository; private readonly Repository<CloudPanel> CloudPanelRepository;
private readonly WebSpaceService WebSpaceService; private readonly WebSpaceService WebSpaceService;
private readonly NodeService NodeService; private readonly NodeService NodeService;
private readonly ConfigService ConfigService;
public SmartDeployService( public SmartDeployService(
NodeRepository nodeRepository, NodeRepository nodeRepository,
NodeService nodeService, NodeService nodeService,
WebSpaceService webSpaceService, WebSpaceService webSpaceService,
Repository<CloudPanel> cloudPanelRepository) Repository<CloudPanel> cloudPanelRepository,
ConfigService configService)
{ {
NodeRepository = nodeRepository; NodeRepository = nodeRepository;
NodeService = nodeService; NodeService = nodeService;
WebSpaceService = webSpaceService; WebSpaceService = webSpaceService;
CloudPanelRepository = cloudPanelRepository; CloudPanelRepository = cloudPanelRepository;
ConfigService = configService;
} }
public async Task<Node?> GetNode() public async Task<Node?> GetNode()
{ {
var config = ConfigService
.GetSection("Moonlight")
.GetSection("SmartDeploy")
.GetSection("Server");
if (config.GetValue<bool>("EnableOverride"))
{
var nodeId = config.GetValue<int>("OverrideNode");
return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId);
}
var data = new Dictionary<Node, double>(); var data = new Dictionary<Node, double>();
foreach (var node in NodeRepository.Get().ToArray()) foreach (var node in NodeRepository.Get().ToArray())
@@ -59,17 +74,17 @@ public class SmartDeployService
try try
{ {
var cpuStats = await NodeService.GetCpuStats(node); var cpuMetrics = await NodeService.GetCpuMetrics(node);
var memoryStats = await NodeService.GetMemoryStats(node); var memoryMetrics = await NodeService.GetMemoryMetrics(node);
var diskStats = await NodeService.GetDiskStats(node); var diskMetrics = await NodeService.GetDiskMetrics(node);
var cpuWeight = 0.5; // Weight of CPU usage in the final score var cpuWeight = 0.5; // Weight of CPU usage in the final score
var memoryWeight = 0.3; // Weight of memory usage in the final score var memoryWeight = 0.3; // Weight of memory usage in the final score
var diskSpaceWeight = 0.2; // Weight of free disk space in the final score var diskSpaceWeight = 0.2; // Weight of free disk space in the final score
var cpuScore = (1 - cpuStats.Usage) * cpuWeight; // CPU score is based on the inverse of CPU usage var cpuScore = (1 - cpuMetrics.CpuUsage) * cpuWeight; // CPU score is based on the inverse of CPU usage
var memoryScore = (1 - (memoryStats.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory var memoryScore = (1 - (memoryMetrics.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory
var diskSpaceScore = (double) diskStats.FreeBytes / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB var diskSpaceScore = (double) (diskMetrics.Total - diskMetrics.Used) / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB
var finalScore = cpuScore + memoryScore + diskSpaceScore; var finalScore = cpuScore + memoryScore + diskSpaceScore;

View File

@@ -7,20 +7,33 @@ namespace Moonlight.App.Services.Statistics;
public class StatisticsViewService public class StatisticsViewService
{ {
private readonly StatisticsRepository StatisticsRepository; private readonly StatisticsRepository StatisticsRepository;
private readonly Repository<User> UserRepository;
private readonly DateTimeService DateTimeService; private readonly DateTimeService DateTimeService;
public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService) public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService, Repository<User> userRepository)
{ {
StatisticsRepository = statisticsRepository; StatisticsRepository = statisticsRepository;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
UserRepository = userRepository;
} }
public StatisticsData[] GetData(string chart, StatisticsTimeSpan timeSpan) public StatisticsData[] GetData(string chart, StatisticsTimeSpan timeSpan)
{ {
var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan); var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan);
var objs = StatisticsRepository.Get().Where(x => x.Date > startDate && x.Chart == chart); var objs = StatisticsRepository
.Get()
.Where(x => x.Date > startDate && x.Chart == chart);
return objs.ToArray(); return objs.ToArray();
} }
public int GetActiveUsers(StatisticsTimeSpan timeSpan)
{
var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan);
return UserRepository
.Get()
.Count(x => x.LastVisitedAt > startDate);
}
} }

View File

@@ -72,11 +72,21 @@ public class WebSpaceService
public async Task Delete(WebSpace w) public async Task Delete(WebSpace w)
{ {
var website = EnsureData(w); var webSpace = WebSpaceRepository
.Get()
.Include(x => x.Databases)
.Include(x => x.CloudPanel)
.Include(x => x.Owner)
.First(x => x.Id == w.Id);
await CloudPanelApiHelper.Delete(website.CloudPanel, $"site/{website.Domain}", null); foreach (var database in webSpace.Databases.ToArray())
{
await DeleteDatabase(webSpace, database);
}
WebSpaceRepository.Delete(website); await CloudPanelApiHelper.Delete(webSpace.CloudPanel, $"site/{webSpace.Domain}", null);
WebSpaceRepository.Delete(webSpace);
} }
public async Task<bool> IsHostUp(CloudPanel cloudPanel) public async Task<bool> IsHostUp(CloudPanel cloudPanel)

View File

@@ -20,6 +20,7 @@ FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
RUN mkdir -p /app/storage RUN mkdir -p /app/storage
RUN touch /app/storage/donttriggeranyerrors
RUN rm -r /app/storage/* RUN rm -r /app/storage/*
COPY "Moonlight/defaultstorage" "/app/defaultstorage" COPY "Moonlight/defaultstorage" "/app/defaultstorage"
ENTRYPOINT ["dotnet", "Moonlight.dll"] ENTRYPOINT ["dotnet", "Moonlight.dll"]

View File

@@ -9,6 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" />
<PackageReference Include="Blazor-ApexCharts" Version="0.9.16-beta" /> <PackageReference Include="Blazor-ApexCharts" Version="0.9.16-beta" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" /> <PackageReference Include="Ben.Demystifier" Version="0.4.1" />
@@ -23,6 +24,7 @@
<PackageReference Include="FluentFTP" Version="46.0.2" /> <PackageReference Include="FluentFTP" Version="46.0.2" />
<PackageReference Include="GravatarSharp.Core" Version="1.0.1.2" /> <PackageReference Include="GravatarSharp.Core" Version="1.0.1.2" />
<PackageReference Include="JWT" Version="10.0.2" /> <PackageReference Include="JWT" Version="10.0.2" />
<PackageReference Include="LibGit2Sharp" Version="0.27.2" />
<PackageReference Include="Logging.Net" Version="1.1.3" /> <PackageReference Include="Logging.Net" Version="1.1.3" />
<PackageReference Include="MailKit" Version="4.0.0" /> <PackageReference Include="MailKit" Version="4.0.0" />
<PackageReference Include="Mappy.Net" Version="1.0.2" /> <PackageReference Include="Mappy.Net" Version="1.0.2" />
@@ -39,9 +41,9 @@
<PackageReference Include="MineStat" Version="3.1.1" /> <PackageReference Include="MineStat" Version="3.1.1" />
<PackageReference Include="MySqlBackup.NET" Version="2.3.8" /> <PackageReference Include="MySqlBackup.NET" Version="2.3.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3-beta1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3-beta1" />
<PackageReference Include="Octokit" Version="6.0.0" />
<PackageReference Include="Otp.NET" Version="1.3.0" /> <PackageReference Include="Otp.NET" Version="1.3.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="PteroConsole.NET" Version="1.0.4" />
<PackageReference Include="QRCoder" Version="1.4.3" /> <PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="RestSharp" Version="109.0.0-preview.1" /> <PackageReference Include="RestSharp" Version="109.0.0-preview.1" />
<PackageReference Include="SSH.NET" Version="2020.0.2" /> <PackageReference Include="SSH.NET" Version="2020.0.2" />
@@ -72,9 +74,9 @@
<ItemGroup> <ItemGroup>
<Folder Include="App\ApiClients\CloudPanel\Resources\" /> <Folder Include="App\ApiClients\CloudPanel\Resources\" />
<Folder Include="App\ApiClients\Daemon\Requests\" />
<Folder Include="App\Http\Middleware" /> <Folder Include="App\Http\Middleware" />
<Folder Include="storage\backups\" /> <Folder Include="storage\backups\" />
<Folder Include="storage\resources\public\background\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,6 +2,7 @@
@using Moonlight.App.Extensions @using Moonlight.App.Extensions
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.Services.Sessions
@namespace Moonlight.Pages @namespace Moonlight.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@@ -99,7 +100,6 @@
<script src="/assets/plugins/global/plugins.bundle.js"></script> <script src="/assets/plugins/global/plugins.bundle.js"></script>
<script src="/_content/XtermBlazor/XtermBlazor.min.js"></script> <script src="/_content/XtermBlazor/XtermBlazor.min.js"></script>
<script src="/_content/BlazorTable/BlazorTable.min.js"></script> <script src="/_content/BlazorTable/BlazorTable.min.js"></script>
<script src="/_content/BlazorInputFile/inputfile.js"></script>
<script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script> <script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
<script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script> <script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
@@ -123,5 +123,6 @@ moonlight.loading.registerXterm();
<script src="_content/Blazor-ApexCharts/js/apex-charts.min.js"></script> <script src="_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
<script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script> <script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,14 +1,17 @@
using BlazorDownloadFile; using BlazorDownloadFile;
using BlazorTable; using BlazorTable;
using CurrieTechnologies.Razor.SweetAlert2; using CurrieTechnologies.Razor.SweetAlert2;
using HealthChecks.UI.Client;
using Logging.Net; using Logging.Net;
using Moonlight.App.ApiClients.CloudPanel; using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.ApiClients.Daemon; using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Paper; using Moonlight.App.ApiClients.Paper;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database; using Moonlight.App.Database;
using Moonlight.App.Diagnostics.HealthChecks;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.LogMigrator; using Moonlight.App.LogMigrator;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Domains; using Moonlight.App.Repositories.Domains;
@@ -31,16 +34,19 @@ namespace Moonlight
{ {
public class Program public class Program
{ {
// App version. Change for release public static async Task Main(string[] args)
public static readonly string AppVersion = $"InDev {Formatter.FormatDateOnly(DateTime.Now.Date)}";
public static void Main(string[] args)
{ {
Logger.UsedLogger = new CacheLogger(); Logger.UsedLogger = new CacheLogger();
Logger.Info($"Working dir: {Directory.GetCurrentDirectory()}"); Logger.Info($"Working dir: {Directory.GetCurrentDirectory()}");
DatabaseCheckup.Perform(); Logger.Info("Running pre-init tasks");
// This will also copy all default config files
var configService = new ConfigService(new StorageService());
var databaseCheckupService = new DatabaseCheckupService(configService);
await databaseCheckupService.Perform();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -59,6 +65,10 @@ namespace Moonlight
options.HandshakeTimeout = TimeSpan.FromSeconds(10); options.HandshakeTimeout = TimeSpan.FromSeconds(10);
}); });
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("Database")
.AddCheck<NodeHealthCheck>("Nodes")
.AddCheck<DaemonHealthCheck>("Daemons");
// Databases // Databases
builder.Services.AddDbContext<DataContext>(); builder.Services.AddDbContext<DataContext>();
@@ -121,6 +131,7 @@ namespace Moonlight
builder.Services.AddScoped<ReCaptchaService>(); builder.Services.AddScoped<ReCaptchaService>();
builder.Services.AddScoped<IpBanService>(); builder.Services.AddScoped<IpBanService>();
builder.Services.AddSingleton<OAuth2Service>(); builder.Services.AddSingleton<OAuth2Service>();
builder.Services.AddScoped<DynamicBackgroundService>();
builder.Services.AddScoped<SubscriptionService>(); builder.Services.AddScoped<SubscriptionService>();
builder.Services.AddScoped<SubscriptionAdminService>(); builder.Services.AddScoped<SubscriptionAdminService>();
@@ -155,6 +166,9 @@ namespace Moonlight
builder.Services.AddSingleton<DiscordNotificationService>(); builder.Services.AddSingleton<DiscordNotificationService>();
builder.Services.AddSingleton<CleanupService>(); builder.Services.AddSingleton<CleanupService>();
// Other
builder.Services.AddSingleton<MoonlightService>();
// Third party services // Third party services
builder.Services.AddBlazorTable(); builder.Services.AddBlazorTable();
builder.Services.AddSweetAlert2(options => { options.Theme = SweetAlertTheme.Dark; }); builder.Services.AddSweetAlert2(options => { options.Theme = SweetAlertTheme.Dark; });
@@ -179,6 +193,10 @@ namespace Moonlight
app.MapBlazorHub(); app.MapBlazorHub();
app.MapFallbackToPage("/_Host"); app.MapFallbackToPage("/_Host");
app.MapHealthChecks("/_health", new()
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
// AutoStart services // AutoStart services
_ = app.Services.GetRequiredService<CleanupService>(); _ = app.Services.GetRequiredService<CleanupService>();
@@ -186,10 +204,12 @@ namespace Moonlight
_ = app.Services.GetRequiredService<StatisticsCaptureService>(); _ = app.Services.GetRequiredService<StatisticsCaptureService>();
_ = app.Services.GetRequiredService<DiscordNotificationService>(); _ = app.Services.GetRequiredService<DiscordNotificationService>();
_ = app.Services.GetRequiredService<MoonlightService>();
// Discord bot service // Discord bot service
//var discordBotService = app.Services.GetRequiredService<DiscordBotService>(); //var discordBotService = app.Services.GetRequiredService<DiscordBotService>();
app.Run(); await app.RunAsync();
} }
} }
} }

View File

@@ -1,18 +1,11 @@
{ {
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:26548",
"sslPort": 44339
}
},
"profiles": { "profiles": {
"Moonlight": { "Moonlight": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development",
"ML_DEBUG": "true"
}, },
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118", "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true "dotnetRunMessages": true
@@ -22,7 +15,8 @@
"launchBrowser": true, "launchBrowser": true,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_ENVIRONMENT": "Development",
"ML_SQL_DEBUG": "true" "ML_SQL_DEBUG": "true",
"ML_DEBUG": "true"
}, },
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118", "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true "dotnetRunMessages": true
@@ -32,17 +26,11 @@
"launchBrowser": false, "launchBrowser": false,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_ENVIRONMENT": "Development",
"ML_SQL_DEBUG": "true" "ML_SQL_DEBUG": "true",
"ML_DEBUG": "true"
}, },
"applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118", "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
"dotnetRunMessages": true "dotnetRunMessages": true
},
"Docker": {
"commandName": "Docker",
"launchBrowser": false,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": true
} }
} }
} }

View File

@@ -185,6 +185,8 @@ else
await ToastService.Info(SmartTranslateService.Translate("Starting download")); 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"));
} }
}); });

View File

@@ -31,12 +31,17 @@
</a> </a>
</li> </li>
<li class="nav-item mt-2"> <li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/profile/security"> <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/profile/discord">
<TL>Discord</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/profile/security">
<TL>Security</TL> <TL>Security</TL>
</a> </a>
</li> </li>
<li class="nav-item mt-2"> <li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/profile/subscriptions"> <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/profile/subscriptions">
<TL>Subscriptions</TL> <TL>Subscriptions</TL>
</a> </a>
</li> </li>

View File

@@ -0,0 +1,59 @@
@using Moonlight.App.Models.Misc
@using System.Text
@using Moonlight.App.Helpers
@{
string GetStatusColor(string s)
{
if (s == "Healthy")
return "success";
else if (s == "Unhealthy")
return "danger";
else
return "warning";
}
}
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Moonlight health</TL>:
<div class="ps-3 text-@(GetStatusColor(HealthCheck.Status))">
<TL>@(HealthCheck.Status)</TL>
</div>
</div>
</div>
<div class="card-body">
<div class="accordion" id="healthCheck">
@foreach (var entry in HealthCheck.Entries)
{
<div class="accordion-item">
<h2 class="accordion-header" id="healthCheck_1_header_@(entry.Key.ToLower())">
<button class="accordion-button fs-4 fw-semibold text-@(GetStatusColor(entry.Value.Status))" type="button" data-bs-toggle="collapse" data-bs-target="#healthCheck_body_@(entry.Key.ToLower())">
@(entry.Key)
</button>
</h2>
<div id="healthCheck_body_@(entry.Key.ToLower())" class="accordion-collapse collapse" data-bs-parent="#healthCheck">
<div class="accordion-body">
<b><TL>Status</TL>:</b>&nbsp;<TL>@(entry.Value.Status)</TL><br/>
<b><TL>Description</TL>:</b>&nbsp;@(entry.Value.Description)<br/>
<br/>
@foreach (var x in entry.Value.Data)
{
<b>@(x.Key)</b>
<br/>
@(x.Value)<br/>
}
</div>
</div>
</div>
}
</div>
</div>
</div>
@code
{
[Parameter]
public HealthCheck HealthCheck { get; set; }
}

View File

@@ -1,6 +1,4 @@
@using PteroConsole.NET @using Moonlight.App.Services
@using Moonlight.App.Services
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Helpers @using Moonlight.App.Helpers
@using Logging.Net @using Logging.Net
@using BlazorContextMenu @using BlazorContextMenu
@@ -101,9 +99,6 @@
@code @code
{ {
[CascadingParameter]
public PteroConsole Console { get; set; }
[CascadingParameter] [CascadingParameter]
public Server CurrentServer { get; set; } public Server CurrentServer { get; set; }

View File

@@ -1,11 +1,9 @@
@using PteroConsole.NET @using Moonlight.App.Helpers
@using PteroConsole.NET.Enums
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Services @using Moonlight.App.Services
@using Logging.Net
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers.Wings
@using Moonlight.App.Helpers.Wings.Data
@using Moonlight.App.Services.Interop @using Moonlight.App.Services.Interop
@using Moonlight.Shared.Components.Xterm @using Moonlight.Shared.Components.Xterm
@@ -37,7 +35,7 @@
@code @code
{ {
[CascadingParameter] [CascadingParameter]
public PteroConsole Console { get; set; } public WingsConsole Console { get; set; }
[CascadingParameter] [CascadingParameter]
public Server CurrentServer { get; set; } public Server CurrentServer { get; set; }
@@ -51,13 +49,26 @@
Console.OnMessage += OnMessage; Console.OnMessage += OnMessage;
} }
private async void OnMessage(object? sender, string e) private async void OnMessage(object? sender, ConsoleMessage message)
{ {
if (Terminal != null) 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"); 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");
}
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("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("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("Ensuring file permissions are set correctly, this could take a few seconds...", TranslationService.Translate("Checking file permissions"));
@@ -68,6 +79,7 @@
await Terminal.WriteLine(s); await Terminal.WriteLine(s);
} }
} }
}
public void Dispose() public void Dispose()
{ {
@@ -85,9 +97,9 @@
private void RunOnFirstRender() 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); OnMessage(null, message);
} }

View File

@@ -4,6 +4,7 @@
@using Moonlight.App.Helpers.Files @using Moonlight.App.Helpers.Files
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.ApiClients.Wings @using Moonlight.App.ApiClients.Wings
@using Moonlight.App.Helpers.Wings
@inject WingsApiHelper WingsApiHelper @inject WingsApiHelper WingsApiHelper
@inject WingsJwtHelper WingsJwtHelper @inject WingsJwtHelper WingsJwtHelper

View File

@@ -1,9 +1,8 @@
@using PteroConsole.NET @using Moonlight.App.Services
@using PteroConsole.NET.Enums
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Services
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers @using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Wings
@using Moonlight.App.Helpers.Wings.Enums
@inject SmartTranslateService TranslationService @inject SmartTranslateService TranslationService
@@ -77,32 +76,32 @@
case ServerState.Starting: case ServerState.Starting:
<span class="text-warning"><TL>Starting</TL></span> <span class="text-warning"><TL>Starting</TL></span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.ServerResource.Uptime)))</span> <span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break; break;
case ServerState.Stopping: case ServerState.Stopping:
<span class="text-warning"><TL>Stopping</TL></span> <span class="text-warning"><TL>Stopping</TL></span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.ServerResource.Uptime)))</span> <span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break; break;
case ServerState.Running: case ServerState.Running:
<span class="text-success"><TL>Online</TL></span> <span class="text-success"><TL>Online</TL></span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.ServerResource.Uptime)))</span> <span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break; break;
} }
</span> </span>
</div> </div>
<div class="col fs-5"> <div class="col fs-5">
<span class="fw-bold"><TL>Cpu</TL>:</span> <span class="fw-bold"><TL>Cpu</TL>:</span>
<span class="ms-1 text-muted">@(Math.Round(Console.ServerResource.CpuAbsolute, 2))%</span> <span class="ms-1 text-muted">@(Math.Round(Console.Resource.CpuAbsolute / (CurrentServer.Cpu / 100f), 2))%</span>
</div> </div>
<div class="col fs-5"> <div class="col fs-5">
<span class="fw-bold"><TL>Memory</TL>:</span> <span class="fw-bold"><TL>Memory</TL>:</span>
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.ServerResource.MemoryBytes)) / @(Formatter.FormatSize(Console.ServerResource.MemoryLimitBytes))</span> <span class="ms-1 text-muted">@(Formatter.FormatSize(Console.Resource.MemoryBytes)) / @(Formatter.FormatSize(Console.Resource.MemoryLimitBytes))</span>
</div> </div>
<div class="col fs-5"> <div class="col fs-5">
<span class="fw-bold"><TL>Disk</TL>:</span> <span class="fw-bold"><TL>Disk</TL>:</span>
<span class="ms-1 text-muted">@(Formatter.FormatSize(Console.ServerResource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB</span> <span class="ms-1 text-muted">@(Formatter.FormatSize(Console.Resource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB</span>
</div> </div>
</div> </div>
</div> </div>
@@ -178,7 +177,7 @@
public User User { get; set; } public User User { get; set; }
[CascadingParameter] [CascadingParameter]
public PteroConsole Console { get; set; } public WingsConsole Console { get; set; }
[Parameter] [Parameter]
public RenderFragment ChildContent { get; set; } public RenderFragment ChildContent { get; set; }
@@ -190,8 +189,8 @@
protected override void OnInitialized() protected override void OnInitialized()
{ {
Console.OnServerStateUpdated += async (sender, state) => { await InvokeAsync(StateHasChanged); }; Console.OnServerStateUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); };
Console.OnServerResourceUpdated += async (sender, x) => { await InvokeAsync(StateHasChanged); }; Console.OnResourceUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); };
} }
#region Power Actions #region Power Actions

View File

@@ -5,9 +5,11 @@
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Servers @using Moonlight.App.Repositories.Servers
@using Logging.Net @using Logging.Net
@using Moonlight.App.ApiClients.Wings
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@inject ServerRepository ServerRepository @inject ServerRepository ServerRepository
@inject ServerService ServerService
@inject SmartTranslateService TranslationService @inject SmartTranslateService TranslationService
<div class="col"> <div class="col">
@@ -28,7 +30,8 @@
OnClick="Save" OnClick="Save"
Text="@(TranslationService.Translate("Change"))" Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))" WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary"></WButton> CssClasses="btn-primary">
</WButton>
</td> </td>
</tr> </tr>
</table> </table>
@@ -58,6 +61,20 @@
ServerRepository.Update(CurrentServer); 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(); await Loader.Reload();
} }
} }

View File

@@ -23,7 +23,7 @@
if (await AlertService.ConfirmMath()) if (await AlertService.ConfirmMath())
{ {
await ServerService.Delete(CurrentServer); await ServerService.Delete(CurrentServer);
NavigationManager.NavigateTo("/servers", true); NavigationManager.NavigateTo("/servers");
} }
} }
} }

View File

@@ -10,7 +10,12 @@
<div class="col-xl-4 mb-xl-10"> <div class="col-xl-4 mb-xl-10">
<div class="card h-md-100"> <div class="card h-md-100">
<div class="card-body d-flex flex-column flex-center"> <div class="card-body d-flex flex-column flex-center">
<img class="img-fluid" src="https://shs.moonlightpanel.xyz/api/screenshot?url=http://@(CurrentWebSpace.Domain)" alt="Website screenshot"/> <div class="position-relative" style="width: 100%; height: 0; padding-bottom: 56.25%;">
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center">
<img class="img-fluid" src="/assets/media/gif/loading.gif" alt="Placeholder" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;">
<div class="position-absolute top-0 start-0 w-100 h-100" style="background-image: url('https://shs.moonlightpanel.xyz/api/screenshot?url=http://@(CurrentWebSpace.Domain)'); background-size: cover; background-position: center;"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,16 @@
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject SmartTranslateService SmartTranslateService
@inject WebSpaceService WebSpaceService
@inject NavigationManager NavigationManager
@inject AlertService AlertService
<div class="card card-body"> <div class="card card-body">
<div class="row"> <div class="row">
<div class="col-8"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center"> <div class="d-flex">
<div class="symbol symbol-circle me-5"> <div class="symbol symbol-circle me-5">
<div class="symbol-label bg-transparent text-primary border border-secondary border-dashed"> <div class="symbol-label bg-transparent text-primary border border-secondary border-dashed">
<i class="bx bx-globe bx-md"></i> <i class="bx bx-globe bx-md"></i>
@@ -14,13 +21,17 @@
<div class="text-muted fs-5">@(WebSpace.CloudPanel.Name)</div> <div class="text-muted fs-5">@(WebSpace.CloudPanel.Name)</div>
</div> </div>
</div> </div>
<WButton Text="@(SmartTranslateService.Translate("Delete"))"
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
OnClick="Delete"
CssClasses="btn-danger">
</WButton>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="my-5"></div>
<div class="separator my-5"></div> <div class="card mb-xl-10 mb-5">
</div>
<div class="card mb-5 mb-xl-10">
<div class="card-body pt-0 pb-0"> <div class="card-body pt-0 pb-0">
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold"> <ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2"> <li class="nav-item mt-2">
@@ -54,4 +65,13 @@
[Parameter] [Parameter]
public WebSpace WebSpace { get; set; } public WebSpace WebSpace { get; set; }
private async Task Delete()
{
if (await AlertService.ConfirmMath())
{
await WebSpaceService.Delete(WebSpace);
NavigationManager.NavigateTo("/webspaces");
}
}
} }

View File

@@ -22,7 +22,7 @@
</label> </label>
</div> </div>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="21"> <input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="22">
</div> </div>
</div> </div>
<div class="row fv-row mb-7"> <div class="row fv-row mb-7">

View File

@@ -20,6 +20,7 @@
@inject ToastService ToastService @inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService @inject SmartTranslateService SmartTranslateService
@inject IpBanService IpBanService @inject IpBanService IpBanService
@inject DynamicBackgroundService DynamicBackgroundService
<GlobalErrorBoundary> <GlobalErrorBoundary>
@{ @{
@@ -56,7 +57,7 @@
<Sidebar></Sidebar> <Sidebar></Sidebar>
<div class="app-main flex-column flex-row-fluid" id="kt_app_main"> <div class="app-main flex-column flex-row-fluid" id="kt_app_main">
<div class="d-flex flex-column flex-column-fluid"> <div class="d-flex flex-column flex-column-fluid">
<div id="kt_app_content" class="app-content flex-column-fluid"> <div id="kt_app_content" class="app-content flex-column-fluid" style="background-position: center; background-size: cover; background-repeat: no-repeat; background-attachment: fixed; background-image: url('@(DynamicBackgroundService.BackgroundImageUrl)')">
<div id="kt_app_content_container" class="app-container container-fluid"> <div id="kt_app_content_container" class="app-container container-fluid">
<div class="mt-10"> <div class="mt-10">
<SoftErrorBoundary> <SoftErrorBoundary>
@@ -189,15 +190,20 @@
{ {
try try
{ {
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) =>
{
await InvokeAsync(StateHasChanged);
};
IsIpBanned = await IpBanService.IsBanned(); IsIpBanned = await IpBanService.IsBanned();
if(IsIpBanned) if(IsIpBanned)
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Event.On<Object>("ipBan.update", this, async o => await Event.On<Object>("ipBan.update", this, async _ =>
{ {
IsIpBanned = await IpBanService.IsBanned(); IsIpBanned = await IpBanService.IsBanned();
await InvokeAsync(StateHasChanged); NavigationManager.NavigateTo(NavigationManager.Uri, true);
}); });
User = await IdentityService.Get(); User = await IdentityService.Get();
@@ -211,7 +217,13 @@
await SessionService.Register(); await SessionService.Register();
NavigationManager.LocationChanged += (sender, args) => { SessionService.Refresh(); }; NavigationManager.LocationChanged += async (_, _) =>
{
SessionService.Refresh();
if (!NavigationManager.Uri.Contains("/server/"))
await DynamicBackgroundService.Reset();
};
if (User != null) if (User != null)
{ {

View File

@@ -3,11 +3,16 @@
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Domains @using Moonlight.App.Repositories.Domains
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Misc
@using Moonlight.App.Services
@using Newtonsoft.Json
@using Logging.Net
@inject ServerRepository ServerRepository @inject ServerRepository ServerRepository
@inject UserRepository UserRepository @inject UserRepository UserRepository
@inject Repository<WebSpace> WebSpaceRepository @inject Repository<WebSpace> WebSpaceRepository
@inject DomainRepository DomainRepository @inject DomainRepository DomainRepository
@inject ConfigService ConfigService
<OnlyAdmin> <OnlyAdmin>
<LazyLoader Load="Load"> <LazyLoader Load="Load">
@@ -97,6 +102,28 @@
</a> </a>
</div> </div>
</div> </div>
<LazyLoader Load="LoadHealthCheckData">
@if (HealthCheckData == null)
{
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Moonlight health</TL>
</div>
</div>
<div class="card-body">
<div class="alert alert-warning">
<TL>Unable to fetch health check data</TL>
</div>
</div>
</div>
}
else
{
<HealthCheckView HealthCheck="@HealthCheckData"/>
}
</LazyLoader>
</LazyLoader> </LazyLoader>
</OnlyAdmin> </OnlyAdmin>
@@ -107,6 +134,8 @@
private int DomainCount = 0; private int DomainCount = 0;
private int WebSpaceCount = 0; private int WebSpaceCount = 0;
private HealthCheck? HealthCheckData;
private Task Load(LazyLoader lazyLoader) private Task Load(LazyLoader lazyLoader)
{ {
ServerCount = ServerRepository.Get().Count(); ServerCount = ServerRepository.Get().Count();
@@ -116,4 +145,26 @@
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task LoadHealthCheckData(LazyLoader lazyLoader)
{
await lazyLoader.SetText("Loading health check data");
var appUrl = ConfigService
.GetSection("Moonlight")
.GetValue<string>("AppUrl");
try
{
using var client = new HttpClient();
var json = await client.GetStringAsync($"{appUrl}/_health");
HealthCheckData = JsonConvert.DeserializeObject<HealthCheck>(json) ?? new();
}
catch (Exception e)
{
HealthCheckData = null;
Logger.Warn("Unable to fetch health check data");
Logger.Warn(e);
}
}
} }

View File

@@ -41,7 +41,7 @@ else
</div> </div>
<div class="m-0"> <div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1"> <span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (CpuStats == null) @if (CpuMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -50,12 +50,12 @@ else
else else
{ {
<span> <span>
@(CpuStats.Usage)% <TL>of</TL> @(CpuStats.Cores) <TL>Cores used</TL> @(CpuMetrics.CpuUsage)% <TL>of CPU used</TL>
</span> </span>
} }
</span> </span>
<span class="fw-semibold fs-6"> <span class="fw-semibold fs-6">
@if (CpuStats == null) @if (CpuMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -63,7 +63,7 @@ else
} }
else else
{ {
<span>@(CpuStats.Model)</span> <span>@(CpuMetrics.CpuModel)</span>
} }
</span> </span>
</div> </div>
@@ -78,7 +78,7 @@ else
</div> </div>
<div class="m-0"> <div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1"> <span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (MemoryStats == null) @if (MemoryMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -87,32 +87,12 @@ else
else else
{ {
<span> <span>
@(Formatter.FormatSize(MemoryStats.Used * 1024D * 1024D)) <TL>of</TL> @(Formatter.FormatSize(MemoryStats.Total * 1024D * 1024D)) <TL>used</TL> @(Formatter.FormatSize(MemoryMetrics.Used)) <TL>of</TL> @(Formatter.FormatSize(MemoryMetrics.Total)) <TL>memory used</TL>
</span> </span>
} }
</span> </span>
<span class="fw-semibold fs-6"> <span class="fw-semibold fs-6">
@if (MemoryStats == null) @*IDK what to put here*@
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
if (MemoryStats.Sticks.Any())
{
foreach (var stick in SortMemorySticks(MemoryStats.Sticks))
{
<span>@(stick)</span>
<br/>
}
}
else
{
<span>No memory sticks detected</span>
}
}
</span> </span>
</div> </div>
</div> </div>
@@ -126,7 +106,7 @@ else
</div> </div>
<div class="m-0"> <div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1"> <span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (DiskStats == null) @if (DiskMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -135,21 +115,12 @@ else
else else
{ {
<span> <span>
@(Formatter.FormatSize(DiskStats.TotalSize - DiskStats.FreeBytes)) <TL>of</TL> @(Formatter.FormatSize(DiskStats.TotalSize)) <TL>used</TL> @(Formatter.FormatSize(DiskMetrics.Used)) <TL>of</TL> @(Formatter.FormatSize(DiskMetrics.Total)) <TL>used</TL>
</span> </span>
} }
</span> </span>
<span class="fw-semibold fs-6"> <span class="fw-semibold fs-6">
@if (DiskStats == null) @*IDK what to put here*@
{
<span class="text-muted">
<TL>Loading</TL>
</span>
}
else
{
<span>@(DiskStats.Name) - @(DiskStats.DriveFormat)</span>
}
</span> </span>
</div> </div>
</div> </div>
@@ -239,7 +210,7 @@ else
</div> </div>
<div class="m-0"> <div class="m-0">
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1"> <span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
@if (ContainerStats == null) @if (DockerMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -248,12 +219,12 @@ else
else else
{ {
<span> <span>
<TL>@(ContainerStats.Containers.Count)</TL> <TL>@(DockerMetrics.Containers.Length)</TL>
</span> </span>
} }
</span> </span>
<span class="fw-semibold fs-6"> <span class="fw-semibold fs-6">
@if (ContainerStats == null) @if (DockerMetrics == null)
{ {
<span class="text-muted"> <span class="text-muted">
<TL>Loading</TL> <TL>Loading</TL>
@@ -290,11 +261,11 @@ else
private Node? Node; private Node? Node;
private CpuStats CpuStats; private CpuMetrics CpuMetrics;
private MemoryStats MemoryStats; private MemoryMetrics MemoryMetrics;
private DiskStats DiskStats; private DiskMetrics DiskMetrics;
private DockerMetrics DockerMetrics;
private SystemStatus SystemStatus; private SystemStatus SystemStatus;
private ContainerStats ContainerStats;
private async Task Load(LazyLoader arg) private async Task Load(LazyLoader arg)
{ {
@@ -311,16 +282,16 @@ else
SystemStatus = await NodeService.GetStatus(Node); SystemStatus = await NodeService.GetStatus(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
CpuStats = await NodeService.GetCpuStats(Node); CpuMetrics = await NodeService.GetCpuMetrics(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
MemoryStats = await NodeService.GetMemoryStats(Node); MemoryMetrics = await NodeService.GetMemoryMetrics(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
DiskStats = await NodeService.GetDiskStats(Node); DiskMetrics = await NodeService.GetDiskMetrics(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
ContainerStats = await NodeService.GetContainerStats(Node); DockerMetrics = await NodeService.GetDockerMetrics(Node);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
catch (Exception e) catch (Exception e)
@@ -330,28 +301,4 @@ else
}); });
} }
} }
private List<string> SortMemorySticks(List<MemoryStats.MemoryStick> sticks)
{
// Thank you ChatGPT <3
var groupedMemory = sticks.GroupBy(memory => new { memory.Type, memory.Size })
.Select(group => new
{
Type = group.Key.Type,
Size = group.Key.Size,
Count = group.Count()
});
var sortedMemory = groupedMemory.OrderBy(memory => memory.Type)
.ThenBy(memory => memory.Size);
List<string> sortedList = sortedMemory.Select(memory =>
{
string sizeString = $"{memory.Size}GB";
return $"{memory.Count}x {memory.Type} {sizeString}";
}).ToList();
return sortedList;
}
} }

View File

@@ -38,6 +38,16 @@
</label> </label>
<textarea @bind="Image.Description" type="text" class="form-control"></textarea> <textarea @bind="Image.Description" type="text" class="form-control"></textarea>
</div> </div>
<div class="mb-10">
<label class="form-label">
<TL>Background image url</TL>
</label>
<input
@bind="Image.BackgroundImageUrl"
type="text"
class="form-control"
placeholder="@(SmartTranslateService.Translate("Leave empty for the default background image"))">
</div>
</div> </div>
</div> </div>
<div class="col-xl-6 mb-5 mb-xl-10"> <div class="col-xl-6 mb-5 mb-xl-10">

View File

@@ -132,9 +132,9 @@
try try
{ {
var containerStats = await NodeService.GetContainerStats(node); var dockerMetrics = await NodeService.GetDockerMetrics(node);
foreach (var container in containerStats.Containers) foreach (var container in dockerMetrics.Containers)
{ {
if (Guid.TryParse(container.Name, out Guid uuid)) if (Guid.TryParse(container.Name, out Guid uuid))
{ {

View File

@@ -66,6 +66,20 @@
} }
</div> </div>
} }
<div class="row">
<div class="col-sm-6">
<div class="card mt-4">
<div class="card-header">
<div class="card-title">
<TL>Active users</TL>
</div>
</div>
<div class="card-body">
<span class="fs-2">@(ActiveUsers)</span>
</div>
</div>
</div>
</div>
</LazyLoader> </LazyLoader>
</OnlyAdmin> </OnlyAdmin>
@@ -73,7 +87,9 @@
{ {
private StatisticsTimeSpan StatisticsTimeSpan = StatisticsTimeSpan.Day; private StatisticsTimeSpan StatisticsTimeSpan = StatisticsTimeSpan.Day;
private LazyLoader Loader; private LazyLoader Loader;
private Dictionary<string, StatisticsData[]> Charts = new(); private Dictionary<string, StatisticsData[]> Charts = new();
private int ActiveUsers = 0;
private int TimeSpanBind private int TimeSpanBind
{ {
@@ -91,34 +107,48 @@
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Servers"), SmartTranslateService.Translate("Servers"),
AvgHelper.Calculate(
StatisticsViewService.GetData("serversCount", StatisticsTimeSpan) StatisticsViewService.GetData("serversCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Users"), SmartTranslateService.Translate("Users"),
AvgHelper.Calculate(
StatisticsViewService.GetData("usersCount", StatisticsTimeSpan) StatisticsViewService.GetData("usersCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Domains"), SmartTranslateService.Translate("Domains"),
AvgHelper.Calculate(
StatisticsViewService.GetData("domainsCount", StatisticsTimeSpan) StatisticsViewService.GetData("domainsCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Databases"), SmartTranslateService.Translate("Databases"),
AvgHelper.Calculate(
StatisticsViewService.GetData("databasesCount", StatisticsTimeSpan) StatisticsViewService.GetData("databasesCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Webspaces"), SmartTranslateService.Translate("Webspaces"),
AvgHelper.Calculate(
StatisticsViewService.GetData("webspacesCount", StatisticsTimeSpan) StatisticsViewService.GetData("webspacesCount", StatisticsTimeSpan)
)
); );
Charts.Add( Charts.Add(
SmartTranslateService.Translate("Sessions"), SmartTranslateService.Translate("Sessions"),
AvgHelper.Calculate(
StatisticsViewService.GetData("sessionsCount", StatisticsTimeSpan) StatisticsViewService.GetData("sessionsCount", StatisticsTimeSpan)
)
); );
ActiveUsers = StatisticsViewService.GetActiveUsers(StatisticsTimeSpan);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -2,12 +2,15 @@
@using Moonlight.Shared.Components.Navigations @using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Helpers @using Moonlight.App.Helpers
@using Moonlight.App.Services
@inject HostSystemHelper HostSystemHelper @inject HostSystemHelper HostSystemHelper
@inject MoonlightService MoonlightService
<OnlyAdmin> <OnlyAdmin>
<AdminSystemNavigation Index="0"/> <AdminSystemNavigation Index="0"/>
<LazyLoader Load="Load">
<div class="row"> <div class="row">
<div class="col-xxl-6 my-3"> <div class="col-xxl-6 my-3">
<div class="card"> <div class="card">
@@ -19,7 +22,7 @@
<div class="card-body"> <div class="card-body">
<span class="fs-5"> <span class="fs-5">
<TL>You are running moonlight version</TL> <TL>You are running moonlight version</TL>
<span class="text-primary">@(Program.AppVersion)</span> <span class="text-primary">@(MoonlightService.AppVersion)</span>
</span> </span>
</div> </div>
</div> </div>
@@ -70,5 +73,31 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-xxl-6 my-3">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Uptime</TL>
</span>
</div> </div>
<div class="card-body">
<span class="fs-5">
<TL>Moonlight is since</TL>
<span class="text-primary">
@(Formatter.FormatUptime(DateTime.UtcNow - MoonlightService.StartTimestamp))
</span>
</span>
</div>
</div>
</div>
</div>
</LazyLoader>
</OnlyAdmin> </OnlyAdmin>
@code
{
private Task Load(LazyLoader arg)
{
return Task.CompletedTask;
}
}

View File

@@ -46,7 +46,7 @@
else else
{ {
<Table TableItem="Session" Items="AllSessions" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted"> <Table TableItem="Session" Items="AllSessions" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="Session" Title="@(SmartTranslateService.Translate("Email"))" Field="@(x => x.User.Id)" Sortable="true" Filterable="true" Width="20%"> <Column TableItem="Session" Title="@(SmartTranslateService.Translate("Email"))" Field="@(x => x.User.Email)" Sortable="true" Filterable="true" Width="20%">
<Template> <Template>
@if (context.User == null) @if (context.User == null)
{ {

View File

@@ -1,275 +1,16 @@
@page "/changelog" @page "/changelog"
@using Moonlight.App.Services
@inject MoonlightService MoonlightService
@{ @{
List<string[]> changelog = new List<string[]>
{
new[]
{
"Patch 1, 13.03.2023",
"Service manager"
},
new[]
{
"Patch 2, 14.03.2023",
"Added new image manager. CRUD implemeted"
},
new[]
{
"Patch 3, 20.03.2023",
"Ddos detection",
"Implemented server manager"
},
new[]
{
"Patch 4, 21.03.2023",
"Added user edit form. Fixed edit link"
},
new[]
{
"Patch 5, 24.03.2023",
"Update Discord Bot Branche",
"Removed discord id and discriminator. Fixed oauth2.",
"Updated discord bot branch",
"Updated smart form branch",
"Added smart form",
"Updating PleskIntegration branch",
"Revert \"Updating PleskIntegration branch\""
},
new[]
{
"Patch 6, 27.03.2023",
"Update Index.razor",
"User settings"
},
new[]
{
"Patch 7, 28.03.2023",
"Added form proccessing screen (not finished). Some ui changes. User m."
},
new[]
{
"Patch 8, 02.04.2023",
"Server manage enhancements",
"Cleanup system",
"Update WingsServerConverter.cs",
"Added quick create dropdown",
"fixed login form",
"Login form fix"
},
new[]
{
"Patch 9, 03.04.2023",
"Fixed cleanup",
"Added server reset setting",
"Totp",
"Update from upstream because of database models",
"Audit log",
"Audit log",
"Force password change",
"Support chat redesign",
"Subscriptions",
"Added server rename setting"
},
new[]
{
"Patch 10, 04.04.2023",
"Added server delete. Tweaked setting names",
"Added server node status screen check thingy",
"Update CacheLogger.cs",
"Update to upstream branch",
"Update View.razor",
"Update to upstream branch",
"Removed legacy aaPanel stuff",
"Update DesignFixes",
"Design fixes",
"Create Changelog.razor"
},
new[]
{
"Patch 11, 05.04.2023",
"Ftp file manager",
"Update to upstream branch",
"Added ActivityStatus"
},
new[]
{
"Patch 12, 06.04.2023",
"Domain overview",
"Plesk integration",
"Add websites to statistics branch",
"Removed legacy database ui",
"Replaced legacy dependency resolve with server service function for w.",
"masu is too stupid to use ulong",
"Statistic system",
"host file access + use in admin resource manager"
},
new[]
{
"Patch 13, 10.04.2023",
"Removed old typeahead. Added own solution. Lang file, notes",
"Implemented website order"
},
new[]
{
"Patch 14, 11.04.2023",
"Implemented domain order. Fixed some bugs"
},
new[]
{
"Patch 15, 12.04.2023",
"Implemented new soft error boundary crash handler",
"Added ip locate. Fixed oauth2 mail send. Fixed mail send when generat.",
"Implemented multi allocation",
"register",
"Fixed server lists",
"News page",
"Improved news system"
},
new[]
{
"Patch 16, 13.04.2023",
"domain view redesigned | fixed some masu at midnight",
"Persistent storage",
"Removed old file manager stuff",
"fixed design of delete button",
"redesigned shared domains screen | split in view/add"
},
new[]
{
"Patch 17, 14.04.2023",
"Removed legacy image tags",
"Fixed server deletion and creation allocation bugs"
},
new[]
{
"Patch 18, 15.04.2023",
"Update DiscordBot branch",
"Fix backup delete",
"Implemented default subscription",
"Implemented random loading message",
"Recode frontend js"
},
new[]
{
"Patch 19, 16.04.2023",
"Optimized allocation search. Added sql command log interception",
"Fixed my stupid mistakes",
"Implemented file view loading animation"
},
new[]
{
"Patch 20, 19.04.2023",
"Cloud panel interation"
},
new[]
{
"Patch 21, 20.04.2023",
"Removed legacy website",
"Added user change status try catch, implement logout. password change",
"Added admin check to sftp login system",
"Implemented new event system",
"Rewritten support chat backend. Added discord notifications"
},
new[]
{
"Patch 22, 21.04.2023",
"Removed old support system",
"Deleted old message system. Replaced it with new event system",
"Update ServerNavigation.razor",
"Switched from internal smtp client to MailKit. Added ssl config"
},
new[]
{
"Patch 23, 23.04.2023",
"Added missing refresh call",
"Added better reconnect screens",
"Removed useless logging. Switched to cache mode for support chat",
"Fixed statistics system"
},
new[]
{
"Patch 24, 24.04.2023",
"Add checking user status to login sequence where login form is currently",
"Added config reload button"
},
new[]
{
"Patch 25, 25.04.2023",
"Update Create.razor",
"Added server count for image overview",
"Implemented owner change option in admin ui"
},
new[]
{
"Patch 26, 26.04.2023",
"Image import export",
"Fixed import and export",
"Prevent message duplication with sender check",
"Implemented forge version switcher"
},
new[]
{
"Patch 27, 28.04.2023",
"Improved user details profile page",
"Implemented fabric version setting"
},
new[]
{
"Patch 28, 29.04.2023",
"update my DiscordBot branch",
"Revert \"update my DiscordBot branch\"",
"Discord bot",
"Update to upstream branch"
},
new[]
{
"Patch 29, 02.05.2023",
"Update Index.razor",
"Added multi line feature and file upload in the support chat"
},
new[]
{
"Patch 30, 04.05.2023",
"Implemented new rating system"
},
new[]
{
"Patch 31, 05.05.2023",
"Code cleanup",
"added version and main jar setting",
"fixed server settings design",
"dotnet runtime settings"
},
new[]
{
"Patch 32, 06.05.2023",
"file create"
},
new[]
{
"Patch 33, 17.05.2023",
"Added node selector for server create screen",
"Added website directory limit for website file manager",
"Added new screenhot service",
"Added node and image view to manager. Made the data handling smoother"
},
new[]
{
"Patch 34, 19.05.2023",
"Added recaptcha. Added recaptcha to register page",
"Implemented ip ban"
}
};
changelog.Reverse();
int i = 0; int i = 0;
} }
<div class="card card-docs flex-row-fluid mb-2"> <div class="card card-docs flex-row-fluid mb-2">
<div class="card-body fs-6 py-15 px-10 py-lg-15 px-lg-15 text-gray-700"> <div class="card-body fs-6 py-15 px-10 py-lg-15 px-lg-15 text-gray-700">
<div class="accordion accordion-flush accordion-icon-toggle" id="kt_accordion"> <div class="accordion accordion-flush accordion-icon-toggle" id="kt_accordion">
@foreach (var prt in changelog) @foreach (var prt in MoonlightService.ChangeLog.ToArray().Reverse())
{ {
i++; i++;
<div class="accordion-item mb-5"> <div class="accordion-item mb-5">

View File

@@ -1,5 +1,4 @@
@page "/domain/{Id:int}" @page "/domain/{Id:int}"
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Repositories.Domains @using Moonlight.App.Repositories.Domains
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@@ -11,6 +10,8 @@
@inject DomainRepository DomainRepository @inject DomainRepository DomainRepository
@inject DomainService DomainService @inject DomainService DomainService
@inject SmartTranslateService SmartTranslateService @inject SmartTranslateService SmartTranslateService
@inject NavigationManager NavigationManager
@inject AlertService AlertService
<LazyLoader Load="Load"> <LazyLoader Load="Load">
@if (Domain == null) @if (Domain == null)
@@ -28,6 +29,13 @@
<span class="card-title"> <span class="card-title">
<TL>DNS records for</TL><span class="ms-3">@(domainNameBuilt)</span> <TL>DNS records for</TL><span class="ms-3">@(domainNameBuilt)</span>
</span> </span>
<div class="card-toolbar">
<WButton Text="@(SmartTranslateService.Translate("Delete domain"))"
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
CssClasses="btn-danger"
OnClick="Delete">
</WButton>
</div>
</div> </div>
<div class="mt-5"></div> <div class="mt-5"></div>
<LazyLoader @ref="DnsLazyLoader" Load="LoadDnsRecords"> <LazyLoader @ref="DnsLazyLoader" Load="LoadDnsRecords">
@@ -240,4 +248,13 @@
await DomainService.DeleteDnsRecord(Domain!, record); await DomainService.DeleteDnsRecord(Domain!, record);
await DnsLazyLoader.Reload(); await DnsLazyLoader.Reload();
} }
private async Task Delete()
{
if (await AlertService.ConfirmMath())
{
await DomainService.Delete(Domain!);
NavigationManager.NavigateTo("/domains");
}
}
} }

View File

@@ -125,7 +125,8 @@
</div> </div>
</div> </div>
<!--d-flex flex-row mb-5--> <div class="row">
<div class="col">
<div class="card mb-5"> <div class="card mb-5">
<div class="card-header card-header-stretch"> <div class="card-header card-header-stretch">
<div class="card-title d-flex align-items-center"> <div class="card-title d-flex align-items-center">
@@ -173,14 +174,15 @@
<TL>Create a domain</TL> <TL>Create a domain</TL>
</a> </a>
<span class="text-gray-400 fw-semibold d-block fs-6"> <span class="text-gray-400 fw-semibold d-block fs-6">
<TL>Make your servvices accessible throught your own domain</TL> <TL>Make your services accessible through your own domain</TL>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="col">
<div class="card mb-5"> <div class="card mb-5">
<div class="card-header card-header-stretch"> <div class="card-header card-header-stretch">
<div class="card-title d-flex align-items-center"> <div class="card-title d-flex align-items-center">
@@ -235,6 +237,8 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</LazyLoader> </LazyLoader>
@code @code

View File

@@ -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<User> UserRepository
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="1"/>
@if (User.DiscordId == 0)
{
<div class="row">
<div class="col">
<div class="alert bg-warning d-flex flex-column flex-sm-row p-5 mb-10">
<i class="fs-2hx text-light me-4 mb-5 mb-sm-0 bx bx-error bx-lg"></i>
<div class="d-flex flex-column text-light pe-0 pe-sm-10">
<h4 class="mb-2 light">
<TL>Your account is currently not linked to discord</TL>
</h4>
<TL>To use features like the discord bot, link your moonlight account with your discord account</TL><br/>
</div>
</div>
</div>
<div class="col">
<div class="card card-body">
<a class="btn btn-primary" href="/api/moonlight/oauth2/discord/start">
<TL>Link account</TL>
</a>
</div>
</div>
</div>
}
else
{
<div class="row">
<div class="col">
<div class="alert bg-success d-flex flex-column flex-sm-row p-5 mb-10">
<i class="fs-2hx text-light me-4 mb-5 mb-sm-0 bx bx-check bx-lg"></i>
<div class="d-flex flex-column text-light pe-0 pe-sm-10">
<h4 class="mb-2 light">
<TL>Your account is linked to a discord account</TL>
</h4>
<TL>You are able to use features like the discord bot of moonlight</TL>
</div>
</div>
</div>
<div class="col">
<div class="card card-body">
<WButton CssClasses="btn-danger"
Text="@(SmartTranslateService.Translate("Remove link"))"
WorkingText="@(SmartTranslateService.Translate("Working"))"
OnClick="RemoveLink">
</WButton>
</div>
</div>
</div>
}
@code
{
[CascadingParameter]
public User User { get; set; }
private async Task RemoveLink()
{
User.DiscordId = 0;
UserRepository.Update(User);
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -19,7 +19,7 @@
@inject AlertService AlertService @inject AlertService AlertService
@inject ToastService ToastService @inject ToastService ToastService
<ProfileNavigation Index="1"/> <ProfileNavigation Index="2"/>
<div class="card mb-5 mb-xl-10"> <div class="card mb-5 mb-xl-10">
<LazyLoader Load="Load"> <LazyLoader Load="Load">

View File

@@ -11,7 +11,7 @@
@inject SubscriptionService SubscriptionService @inject SubscriptionService SubscriptionService
@inject SmartTranslateService SmartTranslateService @inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="2"/> <ProfileNavigation Index="3"/>
<div class="card mb-3"> <div class="card mb-3">
<div class="row g-0"> <div class="row g-0">

View File

@@ -1,15 +1,16 @@
@page "/server/{ServerUuid}/{Route?}" @page "/server/{ServerUuid}/{Route?}"
@using PteroConsole.NET
@using Task = System.Threading.Tasks.Task @using Task = System.Threading.Tasks.Task
@using Moonlight.App.Repositories.Servers @using Moonlight.App.Repositories.Servers
@using PteroConsole.NET.Enums
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Logging.Net @using Logging.Net
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Events @using Moonlight.App.Events
@using Moonlight.App.Helpers @using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Wings
@using Moonlight.App.Helpers.Wings.Enums
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.Services.Sessions
@using Moonlight.Shared.Components.Xterm @using Moonlight.Shared.Components.Xterm
@using Moonlight.Shared.Components.ServerControl @using Moonlight.Shared.Components.ServerControl
@using Newtonsoft.Json @using Newtonsoft.Json
@@ -20,6 +21,7 @@
@inject EventSystem Event @inject EventSystem Event
@inject ServerService ServerService @inject ServerService ServerService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject DynamicBackgroundService DynamicBackgroundService
@implements IDisposable @implements IDisposable
@@ -44,7 +46,7 @@
{ {
if (NodeOnline) if (NodeOnline)
{ {
if (Console.ConnectionState == ConnectionState.Connected) if (Console.ConsoleState == ConsoleState.Connected)
{ {
if (Console.ServerState == ServerState.Installing) if (Console.ServerState == ServerState.Installing)
{ {
@@ -179,7 +181,7 @@
[Parameter] [Parameter]
public string? Route { get; set; } public string? Route { get; set; }
private PteroConsole? Console; private WingsConsole? Console;
private Server? CurrentServer; private Server? CurrentServer;
private Node Node; private Node Node;
private bool NodeOnline = false; private bool NodeOnline = false;
@@ -193,11 +195,11 @@
{ {
Console = new(); Console = new();
Console.OnConnectionStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; Console.OnConsoleStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); };
Console.OnServerResourceUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; Console.OnResourceUpdated += (_, _) => { InvokeAsync(StateHasChanged); };
Console.OnServerStateUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; Console.OnServerStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); };
Console.RequestToken += (_) => WingsConsoleHelper.GenerateToken(CurrentServer!); Console.OnRequestNewToken += async _ => await WingsConsoleHelper.GenerateToken(CurrentServer!);
Console.OnMessage += async (_, s) => Console.OnMessage += async (_, s) =>
{ {
@@ -205,7 +207,10 @@
{ {
if (InstallConsole != null) 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 +285,7 @@
await lazyLoader.SetText("Connecting to console"); await lazyLoader.SetText("Connecting to console");
await WingsConsoleHelper.ConnectWings(Console!, CurrentServer); await ReconnectConsole();
await Event.On<Server>($"server.{CurrentServer.Uuid}.installComplete", this, server => await Event.On<Server>($"server.{CurrentServer.Uuid}.installComplete", this, server =>
{ {
@@ -288,6 +293,11 @@
return Task.CompletedTask; return Task.CompletedTask;
}); });
if (string.IsNullOrEmpty(Image.BackgroundImageUrl))
await DynamicBackgroundService.Reset();
else
await DynamicBackgroundService.Change(Image.BackgroundImageUrl);
} }
} }
else else
@@ -296,11 +306,22 @@
} }
} }
private async Task ReconnectConsole()
{
await Console!.Disconnect();
await WingsConsoleHelper.ConnectWings(Console!, CurrentServer!);
}
public async void Dispose() public async void Dispose()
{ {
if (CurrentServer != null) if (CurrentServer != null)
{ {
await Event.Off($"server.{CurrentServer.Uuid}.installComplete", this); await Event.Off($"server.{CurrentServer.Uuid}.installComplete", this);
} }
if (Console != null)
{
Console.Dispose();
}
} }
} }

View File

@@ -1,5 +1,4 @@
@page "/servers" @page "/servers"
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Repositories.Servers @using Moonlight.App.Repositories.Servers
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@@ -11,7 +10,95 @@
<LazyLoader Load="Load"> <LazyLoader Load="Load">
@if (AllServers.Any()) @if (AllServers.Any())
{ {
@foreach (var server in AllServers) if (UseSortedServerView)
{
var groupedServers = AllServers
.OrderBy(x => x.Name)
.GroupBy(x => x.Image.Name);
foreach (var groupedServer in groupedServers)
{
<div class="separator separator-content my-15">@(groupedServer.Key)</div>
<div class="card card-body bg-secondary py-0 my-0 mx-0 px-0">
@foreach (var server in groupedServer)
{
<div class="row mx-4 my-4">
<a class="card card-body" href="/server/@(server.Uuid)">
<div class="row">
<div class="col">
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-3">
<i class="bx bx-md bx-server"></i>
</div>
<div class="d-flex justify-content-start flex-column">
<a href="/server/@(server.Uuid)" class="text-gray-800 text-hover-primary mb-1 fs-5">
@(server.Name)
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
@(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) <span class="text-gray-700">- @(server.Image.Name)</span>
</span>
</div>
</div>
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@(server.Node.Fqdn):@(server.MainAllocation.Port)
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@if (StatusCache.ContainsKey(server))
{
var status = StatusCache[server];
switch (status)
{
case "offline":
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
case "stopping":
<span class="text-warning">
<TL>Stopping</TL>
</span>
break;
case "starting":
<span class="text-warning">
<TL>Starting</TL>
</span>
break;
case "running":
<span class="text-success">
<TL>Running</TL>
</span>
break;
case "failed":
<span class="text-gray-400">
<TL>Failed</TL>
</span>
break;
default:
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
}
}
else
{
<span class="text-gray-400">
<TL>Loading</TL>
</span>
}
</div>
</div>
</a>
</div>
}
</div>
}
}
else
{
foreach (var server in AllServers)
{ {
<div class="row px-5 mb-5"> <div class="row px-5 mb-5">
<a class="card card-body" href="/server/@(server.Uuid)"> <a class="card card-body" href="/server/@(server.Uuid)">
@@ -42,28 +129,42 @@
switch (status) switch (status)
{ {
case "offline": case "offline":
<span class="text-danger"><TL>Offline</TL></span> <span class="text-danger">
<TL>Offline</TL>
</span>
break; break;
case "stopping": case "stopping":
<span class="text-warning"><TL>Stopping</TL></span> <span class="text-warning">
<TL>Stopping</TL>
</span>
break; break;
case "starting": case "starting":
<span class="text-warning"><TL>Starting</TL></span> <span class="text-warning">
<TL>Starting</TL>
</span>
break; break;
case "running": case "running":
<span class="text-success"><TL>Running</TL></span> <span class="text-success">
<TL>Running</TL>
</span>
break; break;
case "failed": case "failed":
<span class="text-gray-400"><TL>Failed</TL></span> <span class="text-gray-400">
<TL>Failed</TL>
</span>
break; break;
default: default:
<span class="text-danger"><TL>Offline</TL></span> <span class="text-danger">
<TL>Offline</TL>
</span>
break; break;
} }
} }
else else
{ {
<span class="text-gray-400"><TL>Loading</TL></span> <span class="text-gray-400">
<TL>Loading</TL>
</span>
} }
</div> </div>
</div> </div>
@@ -71,6 +172,7 @@
</div> </div>
} }
} }
}
else else
{ {
<div class="alert bg-info d-flex flex-column flex-sm-row w-100 p-5"> <div class="alert bg-info d-flex flex-column flex-sm-row w-100 p-5">
@@ -84,6 +186,27 @@
</div> </div>
</div> </div>
} }
<div class="row mt-7 px-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<span class="badge badge-primary badge-lg">Beta</span>
</div>
</div>
<div class="card-body">
<div class="row">
<label class="col-lg-4 col-form-label fw-semibold fs-6">Sorted server view</label>
<div class="col-lg-8 d-flex align-items-center">
<div class="form-check form-check-solid form-switch form-check-custom fv-row">
<input class="form-check-input w-45px h-30px" type="checkbox" id="sortedServerView" @bind="UseSortedServerView">
<label class="form-check-label" for="sortedServerView"></label>
</div>
</div>
</div>
</div>
</div>
</div>
</LazyLoader> </LazyLoader>
@code @code
@@ -94,6 +217,8 @@
private Server[] AllServers; private Server[] AllServers;
private readonly Dictionary<Server, string> StatusCache = new(); private readonly Dictionary<Server, string> StatusCache = new();
private bool UseSortedServerView = false;
private Task Load(LazyLoader arg) private Task Load(LazyLoader arg)
{ {
AllServers = ServerRepository AllServers = ServerRepository
@@ -127,7 +252,7 @@
return Task.CompletedTask; return Task.CompletedTask;
} }
private void AddStatus(App.Database.Entities.Server server, string status) private void AddStatus(Server server, string status)
{ {
lock (StatusCache) lock (StatusCache)
{ {

View File

@@ -8,6 +8,10 @@
"Port": "10324", "Port": "10324",
"Username": "user_name" "Username": "user_name"
}, },
"DiscordBotApi": {
"Enable": false,
"Token": "you api key here"
},
"DiscordBot": { "DiscordBot": {
"Enable": false, "Enable": false,
"Token": "Discord.Token.Here", "Token": "Discord.Token.Here",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB