Implemented node crud and status health check. Added daemon status health endpoint. Refactored project structure. Added sidebar items and ui views

This commit is contained in:
2026-03-05 10:56:52 +00:00
parent 2d1b48b0d4
commit 7c5dc657dc
54 changed files with 1808 additions and 222 deletions

View File

@@ -0,0 +1,8 @@
namespace MoonlightServers.Daemon.Configuration;
public class RemoteOptions
{
public string EndpointUrl { get; set; }
public string TokenId { get; set; }
public string Token { get; set; }
}

View File

@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.Http.Daemon;
namespace MoonlightServers.Daemon.Http.Controllers;
[Authorize]
[ApiController]
[Route("api/health")]
public class HealthController : Controller
{
private readonly RemoteService RemoteService;
public HealthController(RemoteService remoteService)
{
RemoteService = remoteService;
}
[HttpGet]
public async Task<ActionResult<HealthDto>> GetAsync()
{
var remoteStatusCode = await RemoteService.CheckReachabilityAsync();
return new HealthDto()
{
RemoteStatusCode = remoteStatusCode
};
}
}

View File

@@ -0,0 +1,44 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
namespace MoonlightServers.Daemon.Implementations.TokenScheme;
public class TokenSchemeHandler : AuthenticationHandler<TokenSchemeOptions>
{
public const string SchemeName = "MoonlightServers.Token";
public TokenSchemeHandler(
IOptionsMonitor<TokenSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder
) : base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.Request.Headers.TryGetValue(HeaderNames.Authorization, out var authHeaderValues))
return Task.FromResult(AuthenticateResult.Fail("No authorization header present"));
if (authHeaderValues.Count != 1)
return Task.FromResult(AuthenticateResult.Fail("No authorization value present"));
var authHeaderValue = authHeaderValues[0];
if (string.IsNullOrEmpty(authHeaderValue))
return Task.FromResult(AuthenticateResult.Fail("No authorization value present"));
if (authHeaderValue != Options.Token)
return Task.FromResult(AuthenticateResult.Fail("Invalid token provided"));
return Task.FromResult(
AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(new ClaimsIdentity([], nameof(TokenSchemeHandler))),
nameof(TokenSchemeHandler)
))
);
}
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace MoonlightServers.Daemon.Implementations.TokenScheme;
public class TokenSchemeOptions : AuthenticationSchemeOptions
{
public string Token { get; set; }
}

View File

@@ -28,4 +28,8 @@
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,12 @@
using Microsoft.Extensions.Logging.Console;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Implementations.TokenScheme;
using MoonlightServers.Daemon.ServerSystem;
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
using MoonlightServers.Daemon.ServerSystem.Implementations.Local;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.Http;
var builder = WebApplication.CreateBuilder(args);
@@ -18,7 +21,28 @@ builder.Services.AddSingleton<ServerService>();
builder.Services.AddDockerServices();
builder.Services.AddLocalServices();
builder.Services.AddControllers();
builder.Services.AddHttpClient();
builder.Services.AddSingleton<RemoteService>();
builder.Services.AddOptions<RemoteOptions>().BindConfiguration("Moonlight:Remote");
var remoteOptions = new RemoteOptions();
builder.Configuration.Bind("Moonlight:Remote", remoteOptions);
builder.Services.AddControllers()
.AddApplicationPart(typeof(Program).Assembly)
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
});
builder.Services.AddAuthentication(TokenSchemeHandler.SchemeName)
.AddScheme<TokenSchemeOptions, TokenSchemeHandler>(TokenSchemeHandler.SchemeName, options =>
{
options.Token = remoteOptions!.Token;
});
builder.Logging.AddFilter("MoonlightServers.Daemon.Implementations.TokenScheme.TokenSchemeHandler", LogLevel.Warning);
var app = builder.Build();
@@ -50,8 +74,6 @@ Task.Run(async () =>
await server.StopAsync();
Console.ReadLine();
await serverService.DeleteAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
}
catch (Exception e)
{

View File

@@ -0,0 +1,117 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.DaemonShared.Http;
namespace MoonlightServers.Daemon.Services;
public class RemoteService
{
private readonly IOptions<RemoteOptions> Options;
private readonly ILogger<RemoteService> Logger;
private readonly IHttpClientFactory ClientFactory;
public RemoteService(
IOptions<RemoteOptions> options,
IHttpClientFactory clientFactory,
ILogger<RemoteService> logger
)
{
Options = options;
ClientFactory = clientFactory;
Logger = logger;
}
public async Task<int> CheckReachabilityAsync()
{
try
{
var client = ClientFactory.CreateClient();
var request = CreateBaseRequest(HttpMethod.Get, "api/remote/servers/nodes/ping");
var response = await client.SendAsync(request);
return (int)response.StatusCode;
}
catch (Exception e)
{
Logger.LogTrace(e, "An error occured while checking if remote is reachable");
return 0;
}
}
private HttpRequestMessage CreateBaseRequest(
[StringSyntax(StringSyntaxAttribute.Uri)]
HttpMethod method,
string endpoint
)
{
var request = new HttpRequestMessage();
request.Headers.Add(HeaderNames.Authorization, $"{Options.Value.TokenId} {Options.Value.Token}");
request.RequestUri = new Uri(new Uri(Options.Value.EndpointUrl), endpoint);
request.Method = method;
return request;
}
private async Task EnsureSuccessAsync(HttpResponseMessage message)
{
if (message.IsSuccessStatusCode)
return;
try
{
var problemDetails = await message.Content.ReadFromJsonAsync<ProblemDetails>(
SerializationContext.Default.Options
);
if (problemDetails == null)
{
// If we cant handle problem details, we handle it natively
message.EnsureSuccessStatusCode();
return;
}
// Parse into exception
throw new RemoteException(
problemDetails.Type,
problemDetails.Title,
problemDetails.Status,
problemDetails.Detail,
problemDetails.Errors
);
}
catch (JsonException)
{
// If we cant handle problem details, we handle it natively
message.EnsureSuccessStatusCode();
}
}
}
public class RemoteException : Exception
{
public string Type { get; }
public string Title { get; }
public int Status { get; }
public string? Detail { get; }
public Dictionary<string, string[]>? Errors { get; }
public RemoteException(
string type,
string title,
int status,
string? detail = null,
Dictionary<string, string[]>? errors = null)
: base(detail ?? title)
{
Type = type;
Title = title;
Status = status;
Detail = detail;
Errors = errors;
}
}