Added node server sync and delete sync. Cleaned up codebase and extracted calls to apis to services

This commit is contained in:
2025-03-02 19:24:24 +01:00
parent ef7f866ded
commit 30390dab71
25 changed files with 751 additions and 282 deletions

View File

@@ -8,6 +8,7 @@ using MoonCore.Helpers;
using MoonCore.Models; using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Requests.Admin.Servers; using MoonlightServers.Shared.Http.Requests.Admin.Servers;
using MoonlightServers.Shared.Http.Responses.Admin.Servers; using MoonlightServers.Shared.Http.Responses.Admin.Servers;
@@ -24,6 +25,8 @@ public class ServersController : Controller
private readonly DatabaseRepository<ServerVariable> VariableRepository; private readonly DatabaseRepository<ServerVariable> VariableRepository;
private readonly DatabaseRepository<Server> ServerRepository; private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository; private readonly DatabaseRepository<User> UserRepository;
private readonly ILogger<ServersController> Logger;
private readonly ServerService ServerService;
public ServersController( public ServersController(
CrudHelper<Server, ServerDetailResponse> crudHelper, CrudHelper<Server, ServerDetailResponse> crudHelper,
@@ -32,7 +35,10 @@ public class ServersController : Controller
DatabaseRepository<Allocation> allocationRepository, DatabaseRepository<Allocation> allocationRepository,
DatabaseRepository<ServerVariable> variableRepository, DatabaseRepository<ServerVariable> variableRepository,
DatabaseRepository<Server> serverRepository, DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository) DatabaseRepository<User> userRepository,
ILogger<ServersController> logger,
ServerService serverService
)
{ {
CrudHelper = crudHelper; CrudHelper = crudHelper;
StarRepository = starRepository; StarRepository = starRepository;
@@ -41,6 +47,8 @@ public class ServersController : Controller
VariableRepository = variableRepository; VariableRepository = variableRepository;
ServerRepository = serverRepository; ServerRepository = serverRepository;
UserRepository = userRepository; UserRepository = userRepository;
ServerService = serverService;
Logger = logger;
CrudHelper.QueryModifier = servers => servers CrudHelper.QueryModifier = servers => servers
.Include(x => x.Node) .Include(x => x.Node)
@@ -146,7 +154,7 @@ public class ServersController : Controller
foreach (var variable in star.Variables) foreach (var variable in star.Variables)
{ {
var requestVar = request.Variables.FirstOrDefault(x => x.Key == variable.Key); var requestVar = request.Variables.FirstOrDefault(x => x.Key == variable.Key);
var serverVar = new ServerVariable() var serverVar = new ServerVariable()
{ {
Key = variable.Key, Key = variable.Key,
@@ -162,10 +170,23 @@ public class ServersController : Controller
server.Node = node; server.Node = node;
server.Star = star; server.Star = star;
// TODO: Call node
var finalServer = await ServerRepository.Add(server); var finalServer = await ServerRepository.Add(server);
try
{
await ServerService.Sync(finalServer);
}
catch (Exception e)
{
Logger.LogError("Unable to sync server to node the server is assigned to: {e}", e);
// We are deleting the server from the database after the creation has failed
// to ensure we wont have a bugged server in the database which doesnt exist on the node
await ServerRepository.Remove(finalServer);
throw;
}
return CrudHelper.MapToResult(finalServer); return CrudHelper.MapToResult(finalServer);
} }
@@ -186,7 +207,7 @@ public class ServersController : Controller
.Where(x => x.Server == null || x.Server.Id == server.Id) .Where(x => x.Server == null || x.Server.Id == server.Id)
.Where(x => x.Node.Id == server.Node.Id) .Where(x => x.Node.Id == server.Node.Id)
.FirstOrDefaultAsync(x => x.Id == allocationId); .FirstOrDefaultAsync(x => x.Id == allocationId);
// ^ This loads the allocations specified in the request. // ^ This loads the allocations specified in the request.
// Valid allocations are either free ones or ones which are already allocated to this server // Valid allocations are either free ones or ones which are already allocated to this server
@@ -207,31 +228,53 @@ public class ServersController : Controller
// Set allocations // Set allocations
server.Allocations = allocations; server.Allocations = allocations;
// Process variables // Process variables
foreach (var variable in request.Variables) foreach (var variable in request.Variables)
{ {
// Search server variable associated to the variable in the request // Search server variable associated to the variable in the request
var serverVar = server.Variables var serverVar = server.Variables
.FirstOrDefault(x => x.Key == variable.Key); .FirstOrDefault(x => x.Key == variable.Key);
if(serverVar == null) if (serverVar == null)
continue; continue;
// Update value // Update value
serverVar.Value = variable.Value; serverVar.Value = variable.Value;
} }
// TODO: Call node
await ServerRepository.Update(server); await ServerRepository.Update(server);
// Notify the node about the changes
await ServerService.Sync(server);
return CrudHelper.MapToResult(server); return CrudHelper.MapToResult(server);
} }
[HttpDelete("{id:int}")] [HttpDelete("{id:int}")]
public async Task Delete([FromRoute] int id) public async Task Delete([FromRoute] int id, [FromQuery] bool force = false)
{ {
var server = await CrudHelper.GetSingleModel(id);
try
{
// If the sync fails on the node and we aren't forcing the deletion,
// we don't want to delete it from the database yet
await ServerService.SyncDelete(server);
}
catch (Exception e)
{
if (force)
{
Logger.LogWarning(
"An error occured while syncing deletion of a server to the node. Continuing anyways. Error: {e}",
e
);
}
else
throw;
}
await CrudHelper.Delete(id); await CrudHelper.Delete(id);
} }
} }

View File

@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions; using MoonCore.Extended.Abstractions;
using MoonCore.Helpers; using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services; using MoonlightServers.ApiServer.Services;
@@ -15,113 +16,69 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
public class ServerPowerController : Controller public class ServerPowerController : Controller
{ {
private readonly DatabaseRepository<Server> ServerRepository; private readonly DatabaseRepository<Server> ServerRepository;
private readonly NodeService NodeService; private readonly DatabaseRepository<User> UserRepository;
private readonly ServerService ServerService;
public ServerPowerController(DatabaseRepository<Server> serverRepository, NodeService nodeService) public ServerPowerController(
DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository,
ServerService serverService
)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
NodeService = nodeService; UserRepository = userRepository;
ServerService = serverService;
} }
[HttpPost("{serverId:int}/start")] [HttpPost("{serverId:int}/start")]
[Authorize] [Authorize]
public async Task Start([FromRoute] int serverId) public async Task Start([FromRoute] int serverId)
{ {
var server = await GetServerWithPermCheck(serverId); var server = await GetServerById(serverId);
await ServerService.Start(server);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/start");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
} }
[HttpPost("{serverId:int}/stop")] [HttpPost("{serverId:int}/stop")]
[Authorize] [Authorize]
public async Task Stop([FromRoute] int serverId) public async Task Stop([FromRoute] int serverId)
{ {
var server = await GetServerWithPermCheck(serverId); var server = await GetServerById(serverId);
await ServerService.Stop(server);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/stop");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
} }
[HttpPost("{serverId:int}/kill")] [HttpPost("{serverId:int}/kill")]
[Authorize] [Authorize]
public async Task Kill([FromRoute] int serverId) public async Task Kill([FromRoute] int serverId)
{ {
var server = await GetServerWithPermCheck(serverId); var server = await GetServerById(serverId);
await ServerService.Kill(server);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/kill");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
} }
[HttpPost("{serverId:int}/install")] [HttpPost("{serverId:int}/install")]
[Authorize] [Authorize]
public async Task Install([FromRoute] int serverId) public async Task Install([FromRoute] int serverId)
{ {
var server = await GetServerWithPermCheck(serverId); var server = await GetServerById(serverId);
await ServerService.Install(server);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/install");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
} }
private async Task<Server> GetServerWithPermCheck(int serverId, private async Task<Server> GetServerById(int serverId)
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
{ {
var userIdClaim = User.Claims.First(x => x.Type == "userId"); var server = await ServerRepository
var userId = int.Parse(userIdClaim.Value);
var query = ServerRepository
.Get() .Get()
.Include(x => x.Node) as IQueryable<Server>; .Include(x => x.Node)
if (queryModifier != null)
query = queryModifier.Invoke(query);
var server = await query
.FirstOrDefaultAsync(x => x.Id == serverId); .FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null) if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
if (server.OwnerId == userId) // The current user is the owner var userIdClaim = User.Claims.First(x => x.Type == "userId");
return server; var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
var permissions = User.Claims.First(x => x.Type == "permissions").Value.Split(";", StringSplitOptions.RemoveEmptyEntries); if (!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404);
if (PermissionHelper.HasPermission(permissions, "admin.servers.get")) // The current user is an admin
return server;
throw new HttpApiException("No server with this id found", 404); return server;
} }
} }

View File

@@ -3,8 +3,8 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions; using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using MoonCore.Models; using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Extensions; using MoonlightServers.ApiServer.Extensions;
using MoonlightServers.ApiServer.Services; using MoonlightServers.ApiServer.Services;
@@ -17,13 +17,17 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Route("api/client/servers")] [Route("api/client/servers")]
public class ServersController : Controller public class ServersController : Controller
{ {
private readonly ServerService ServerService;
private readonly DatabaseRepository<Server> ServerRepository; private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly NodeService NodeService; private readonly NodeService NodeService;
public ServersController(DatabaseRepository<Server> serverRepository, NodeService nodeService) public ServersController(DatabaseRepository<Server> serverRepository, NodeService nodeService, ServerService serverService, DatabaseRepository<User> userRepository)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
NodeService = nodeService; NodeService = nodeService;
ServerService = serverService;
UserRepository = userRepository;
} }
[HttpGet] [HttpGet]
@@ -71,13 +75,22 @@ public class ServersController : Controller
[Authorize] [Authorize]
public async Task<ServerDetailResponse> Get([FromRoute] int serverId) public async Task<ServerDetailResponse> Get([FromRoute] int serverId)
{ {
var server = await GetServerWithPermCheck( var server = await ServerRepository
serverId, .Get()
query => .Include(x => x.Allocations)
query .Include(x => x.Star)
.Include(x => x.Allocations) .Include(x => x.Node)
.Include(x => x.Star) .FirstOrDefaultAsync(x => x.Id == serverId);
);
if(server == null)
throw new HttpApiException("No server with this id found", 404);
var userIdClaim = User.Claims.First(x => x.Type == "userId");
var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
if(!ServerService.IsAllowedToAccess(user, server))
throw new HttpApiException("No server with this id found", 404);
return new ServerDetailResponse() return new ServerDetailResponse()
{ {
@@ -98,32 +111,20 @@ public class ServersController : Controller
[Authorize] [Authorize]
public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId) public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
{ {
var server = await GetServerWithPermCheck(serverId); var server = await GetServerById(serverId);
var status = await ServerService.GetStatus(server);
var apiClient = await NodeService.CreateApiClient(server.Node); return new ServerStatusResponse()
try
{ {
var data = await apiClient.GetJson<DaemonShared.DaemonSide.Http.Responses.Servers.ServerStatusResponse>( State = status.State.ToServerPowerState()
$"api/servers/{server.Id}/status" };
);
return new ServerStatusResponse()
{
State = data.State.ToServerPowerState()
};
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
} }
[HttpGet("{serverId:int}/ws")] [HttpGet("{serverId:int}/ws")]
[Authorize] [Authorize]
public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId) public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId)
{ {
var server = await GetServerWithPermCheck(serverId); var server = await GetServerById(serverId);
// TODO: Handle transparent node proxy // TODO: Handle transparent node proxy
@@ -135,11 +136,7 @@ public class ServersController : Controller
var url = ""; var url = "";
if (server.Node.UseSsl) url += server.Node.UseSsl ? "https://" : "http://";
url += "https://";
else
url += "http://";
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/ws"; url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/ws";
return new ServerWebSocketResponse() return new ServerWebSocketResponse()
@@ -153,54 +150,33 @@ public class ServersController : Controller
[Authorize] [Authorize]
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId) public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
{ {
var server = await GetServerWithPermCheck(serverId); var server = await GetServerById(serverId);
var apiClient = await NodeService.CreateApiClient(server.Node); var logs = await ServerService.GetLogs(server);
try return new ServerLogsResponse()
{ {
var data = await apiClient.GetJson<DaemonShared.DaemonSide.Http.Responses.Servers.ServerLogsResponse>( Messages = logs.Messages
$"api/servers/{server.Id}/logs" };
);
return new ServerLogsResponse()
{
Messages = data.Messages
};
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
} }
private async Task<Server> GetServerWithPermCheck(int serverId, private async Task<Server> GetServerById(int serverId)
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
{ {
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if(server == null)
throw new HttpApiException("No server with this id found", 404);
var userIdClaim = User.Claims.First(x => x.Type == "userId"); var userIdClaim = User.Claims.First(x => x.Type == "userId");
var userId = int.Parse(userIdClaim.Value); var userId = int.Parse(userIdClaim.Value);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
var query = ServerRepository
.Get() if(!ServerService.IsAllowedToAccess(user, server))
.Include(x => x.Node) as IQueryable<Server>;
if (queryModifier != null)
query = queryModifier.Invoke(query);
var server = await query
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
if (server.OwnerId == userId) // The current user is the owner return server;
return server;
var permissions = User.Claims.First(x => x.Type == "permissions").Value.Split(";", StringSplitOptions.RemoveEmptyEntries);
if (PermissionHelper.HasPermission(permissions, "admin.servers.get")) // The current user is an admin
return server;
throw new HttpApiException("No server with this id found", 404);
} }
} }

View File

@@ -37,7 +37,7 @@ public class ServersController : Controller
var node = await NodeRepository var node = await NodeRepository
.Get() .Get()
.FirstAsync(x => x.TokenId == tokenId); .FirstAsync(x => x.TokenId == tokenId);
var total = await ServerRepository var total = await ServerRepository
.Get() .Get()
.Where(x => x.Node.Id == node.Id) .Where(x => x.Node.Id == node.Id)
@@ -58,47 +58,12 @@ public class ServersController : Controller
foreach (var server in servers) foreach (var server in servers)
{ {
var dockerImage = server.Star.DockerImages var convertedData = ConvertToServerData(server);
.Skip(server.DockerImageIndex)
.FirstOrDefault();
if (dockerImage == null) if (convertedData == null)
{
dockerImage = server.Star.DockerImages
.Skip(server.Star.DefaultDockerImage)
.FirstOrDefault();
}
if (dockerImage == null)
dockerImage = server.Star.DockerImages.LastOrDefault();
if (dockerImage == null)
{
Logger.LogWarning("Unable to map server data for server {id}: No docker image available", server.Id);
continue; continue;
}
serverData.Add(new ServerDataResponse() serverData.Add(convertedData);
{
Id = server.Id,
StartupCommand = server.StartupOverride ?? server.Star.StartupCommand,
Allocations = server.Allocations.Select(x => new AllocationDataResponse()
{
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray(),
Variables = server.Variables.ToDictionary(x => x.Key, x => x.Value),
Bandwidth = server.Bandwidth,
Cpu = server.Cpu,
Disk = server.Disk,
Memory = server.Memory,
OnlineDetection = server.Star.OnlineDetection,
DockerImage = dockerImage.Identifier,
PullDockerImage = dockerImage.AutoPulling,
ParseConiguration = server.Star.ParseConfiguration,
StopCommand = server.Star.StopCommand,
UseVirtualDisk = server.UseVirtualDisk
});
} }
return new PagedData<ServerDataResponse>() return new PagedData<ServerDataResponse>()
@@ -111,6 +76,38 @@ public class ServersController : Controller
}; };
} }
[HttpGet("{id:int}")]
public async Task<ServerDataResponse> Get([FromRoute] int id)
{
// Load the node via the token id
var tokenId = User.Claims.First(x => x.Type == "iss").Value;
var node = await NodeRepository
.Get()
.FirstAsync(x => x.TokenId == tokenId);
// Load the server with the star data attached. We filter by the node to ensure the node can only access
// servers linked to it
var server = await ServerRepository
.Get()
.Where(x => x.Node.Id == node.Id)
.Include(x => x.Star)
.ThenInclude(x => x.DockerImages)
.Include(x => x.Variables)
.Include(x => x.Allocations)
.FirstOrDefaultAsync(x => x.Id == id);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var convertedData = ConvertToServerData(server);
if (convertedData == null)
throw new HttpApiException("An error occured while creating the server data model", 500);
return convertedData;
}
[HttpGet("{id:int}/install")] [HttpGet("{id:int}/install")]
public async Task<ServerInstallDataResponse> GetInstall([FromRoute] int id) public async Task<ServerInstallDataResponse> GetInstall([FromRoute] int id)
{ {
@@ -120,7 +117,7 @@ public class ServersController : Controller
var node = await NodeRepository var node = await NodeRepository
.Get() .Get()
.FirstAsync(x => x.TokenId == tokenId); .FirstAsync(x => x.TokenId == tokenId);
// Load the server with the star data attached. We filter by the node to ensure the node can only access // Load the server with the star data attached. We filter by the node to ensure the node can only access
// servers linked to it // servers linked to it
var server = await ServerRepository var server = await ServerRepository
@@ -139,4 +136,58 @@ public class ServersController : Controller
Shell = server.Star.InstallShell Shell = server.Star.InstallShell
}; };
} }
private ServerDataResponse? ConvertToServerData(Server server)
{
// Find the docker image to use for this server
StarDockerImage? dockerImage = null;
// Handle server set image if specified
if (server.DockerImageIndex != -1)
{
dockerImage = server.Star.DockerImages
.Skip(server.DockerImageIndex)
.FirstOrDefault();
}
// Handle star default image if set
if (dockerImage == null && server.Star.DefaultDockerImage != -1)
{
dockerImage = server.Star.DockerImages
.Skip(server.Star.DefaultDockerImage)
.FirstOrDefault();
}
if (dockerImage == null)
dockerImage = server.Star.DockerImages.LastOrDefault();
if (dockerImage == null)
{
Logger.LogWarning("Unable to map server data for server {id}: No docker image available", server.Id);
return null;
}
// Convert model
return new ServerDataResponse()
{
Id = server.Id,
StartupCommand = server.StartupOverride ?? server.Star.StartupCommand,
Allocations = server.Allocations.Select(x => new AllocationDataResponse()
{
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray(),
Variables = server.Variables.ToDictionary(x => x.Key, x => x.Value),
Bandwidth = server.Bandwidth,
Cpu = server.Cpu,
Disk = server.Disk,
Memory = server.Memory,
OnlineDetection = server.Star.OnlineDetection,
DockerImage = dockerImage.Identifier,
PullDockerImage = dockerImage.AutoPulling,
ParseConiguration = server.Star.ParseConfiguration,
StopCommand = server.Star.StopCommand,
UseVirtualDisk = server.UseVirtualDisk
};
}
} }

View File

@@ -14,27 +14,6 @@ namespace MoonlightServers.ApiServer.Services;
[Singleton] [Singleton]
public class NodeService public class NodeService
{ {
public async Task<HttpApiClient> CreateApiClient(Node node)
{
var url = "";
if (node.UseSsl)
url += "https://";
else
url += "http://";
url += $"{node.Fqdn}:{node.HttpPort}/";
var httpClient = new HttpClient()
{
BaseAddress = new Uri(url)
};
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {node.Token}");
return new HttpApiClient(httpClient);
}
public string CreateAccessToken(Node node, Action<Dictionary<string, object>> parameters, TimeSpan duration) public string CreateAccessToken(Node node, Action<Dictionary<string, object>> parameters, TimeSpan duration)
{ {
var claims = new Dictionary<string, object>(); var claims = new Dictionary<string, object>();
@@ -63,7 +42,7 @@ public class NodeService
public async Task<SystemStatusResponse> GetSystemStatus(Node node) public async Task<SystemStatusResponse> GetSystemStatus(Node node)
{ {
using var apiClient = await CreateApiClient(node); using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<SystemStatusResponse>("api/system/status"); return await apiClient.GetJson<SystemStatusResponse>("api/system/status");
} }
@@ -71,21 +50,66 @@ public class NodeService
public async Task<StatisticsApplicationResponse> GetApplicationStatistics(Node node) public async Task<StatisticsApplicationResponse> GetApplicationStatistics(Node node)
{ {
using var apiClient = await CreateApiClient(node); using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsApplicationResponse>("api/statistics/application"); return await apiClient.GetJson<StatisticsApplicationResponse>("api/statistics/application");
} }
public async Task<StatisticsHostResponse> GetHostStatistics(Node node) public async Task<StatisticsHostResponse> GetHostStatistics(Node node)
{ {
using var apiClient = await CreateApiClient(node); using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsHostResponse>("api/statistics/host"); return await apiClient.GetJson<StatisticsHostResponse>("api/statistics/host");
} }
public async Task<StatisticsDockerResponse> GetDockerStatistics(Node node) public async Task<StatisticsDockerResponse> GetDockerStatistics(Node node)
{ {
using var apiClient = await CreateApiClient(node); using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsDockerResponse>("api/statistics/docker"); return await apiClient.GetJson<StatisticsDockerResponse>("api/statistics/docker");
} }
#endregion #endregion
#region Helpers
private string GenerateJwt(Node node)
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
//Expires = DateTime.UtcNow.AddYears(1),
Expires = DateTime.UtcNow.AddMinutes(1),
NotBefore = DateTime.UtcNow.AddSeconds(-1),
IssuedAt = DateTime.UtcNow,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
node.Token
)),
SecurityAlgorithms.HmacSha256
)
};
var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor);
return jwtSecurityTokenHandler.WriteToken(securityToken);
}
public HttpApiClient CreateApiClient(Node node)
{
var url = "";
url += node.UseSsl ? "https://" : "http://";
url += $"{node.Fqdn}:{node.HttpPort}/";
var httpClient = new HttpClient()
{
BaseAddress = new Uri(url)
};
var jwt = GenerateJwt(node);
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
return new HttpApiClient(httpClient);
}
#endregion
} }

View File

@@ -0,0 +1,165 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
namespace MoonlightServers.ApiServer.Services;
[Scoped]
public class ServerService
{
private readonly NodeService NodeService;
private readonly DatabaseRepository<Server> ServerRepository;
public ServerService(NodeService nodeService, DatabaseRepository<Server> serverRepository)
{
NodeService = nodeService;
ServerRepository = serverRepository;
}
#region Power Actions
public async Task Start(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/start");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task Stop(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/stop");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task Kill(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/kill");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
#endregion
public async Task Install(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/install");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task Sync(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post($"api/servers/{server.Id}/sync");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task SyncDelete(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Delete($"api/servers/{server.Id}");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task<ServerStatusResponse> GetStatus(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
return await apiClient.GetJson<ServerStatusResponse>($"api/servers/{server.Id}/status");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task<ServerLogsResponse> GetLogs(Server server)
{
try
{
using var apiClient = await GetApiClient(server);
return await apiClient.GetJson<ServerLogsResponse>($"api/servers/{server.Id}/logs");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
#region Helpers
public bool IsAllowedToAccess(User user, Server server)
{
if (server.OwnerId == user.Id)
return true;
var permissions = JsonSerializer.Deserialize<string[]>(
user.PermissionsJson
) ?? [];
return PermissionHelper.HasPermission(permissions, "admin.servers.get");
}
private async Task<HttpApiClient> GetApiClient(Server server)
{
var serverWithNode = server;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
// It can be null when its not included when loading via ef !!!
if (server.Node == null)
{
serverWithNode = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstAsync(x => x.Id == server.Id);
}
return NodeService.CreateApiClient(serverWithNode.Node);
}
#endregion
}

View File

@@ -22,9 +22,17 @@ public partial class Server
await LogToConsole("Fetching installation configuration"); await LogToConsole("Fetching installation configuration");
// Fetching remote configuration // Fetching remote configuration and install config
var remoteService = ServiceProvider.GetRequiredService<RemoteService>(); var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
var installData = await remoteService.GetServerInstallation(Configuration.Id); var installData = await remoteService.GetServerInstallation(Configuration.Id);
var serverData = await remoteService.GetServer(Configuration.Id);
// We are updating the regular server config here as well
// as changes to variables and other settings wouldn't sync otherwise
// because they won't trigger a sync
var serverConfiguration = serverData.ToServerConfiguration();
UpdateConfiguration(serverConfiguration);
var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>(); var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>();

View File

@@ -1,5 +1,7 @@
using Docker.DotNet; using Docker.DotNet;
using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Extensions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Abstractions; namespace MoonlightServers.Daemon.Abstractions;
@@ -11,6 +13,17 @@ public partial class Server
{ {
try try
{ {
await LogToConsole("Fetching configuration");
var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
var serverData = await remoteService.GetServer(Configuration.Id);
// We are updating the server config here
// as changes to variables and other settings wouldn't sync otherwise
// because they won't trigger a sync
var serverConfiguration = serverData.ToServerConfiguration();
UpdateConfiguration(serverConfiguration);
await ReCreate(); await ReCreate();
await LogToConsole("Starting container"); await LogToConsole("Starting container");

View File

@@ -49,4 +49,7 @@ public partial class Server
RuntimeContainerName = $"moonlight-runtime-{Configuration.Id}"; RuntimeContainerName = $"moonlight-runtime-{Configuration.Id}";
InstallationContainerName = $"moonlight-install-{Configuration.Id}"; InstallationContainerName = $"moonlight-install-{Configuration.Id}";
} }
public void UpdateConfiguration(ServerConfiguration configuration)
=> Configuration = configuration;
} }

View File

@@ -2,11 +2,35 @@ using Docker.DotNet.Models;
using Mono.Unix.Native; using Mono.Unix.Native;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonlightServers.Daemon.Models.Cache; using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Extensions; namespace MoonlightServers.Daemon.Extensions;
public static class ServerConfigurationExtensions public static class ServerConfigurationExtensions
{ {
public static ServerConfiguration ToServerConfiguration(this ServerDataResponse response)
{
return new ServerConfiguration()
{
Id = response.Id,
StartupCommand = response.StartupCommand,
Allocations = response.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
{
IpAddress = y.IpAddress,
Port = y.Port
}).ToArray(),
Variables = response.Variables,
OnlineDetection = response.OnlineDetection,
DockerImage = response.DockerImage,
UseVirtualDisk = response.UseVirtualDisk,
Bandwidth = response.Bandwidth,
Cpu = response.Cpu,
Disk = response.Disk,
Memory = response.Memory,
StopCommand = response.StopCommand
};
}
public static CreateContainerParameters ToRuntimeCreateParameters(this ServerConfiguration configuration, public static CreateContainerParameters ToRuntimeCreateParameters(this ServerConfiguration configuration,
string hostPath, string containerName) string hostPath, string containerName)
{ {

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Enums;
@@ -5,6 +6,7 @@ using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers; namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[Authorize]
[ApiController] [ApiController]
[Route("api/servers")] [Route("api/servers")]
public class ServerPowerController : Controller public class ServerPowerController : Controller

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonlightServers.Daemon.Services; using MoonlightServers.Daemon.Services;
@@ -6,6 +7,7 @@ using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Http.Controllers.Servers; namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[Authorize]
[ApiController] [ApiController]
[Route("api/servers")] [Route("api/servers")]
public class ServersController : Controller public class ServersController : Controller
@@ -17,18 +19,32 @@ public class ServersController : Controller
ServerService = serverService; ServerService = serverService;
} }
[HttpPost("{serverId:int}/sync")]
public async Task Sync([FromRoute] int serverId)
{
await ServerService.Sync(serverId);
}
[HttpDelete("{serverId:int}")]
public async Task Delete([FromRoute] int serverId)
{
await ServerService.Delete(serverId);
}
[HttpGet("{serverId:int}/status")] [HttpGet("{serverId:int}/status")]
public async Task<ServerStatusResponse> GetStatus(int serverId) public Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
{ {
var server = ServerService.GetServer(serverId); var server = ServerService.GetServer(serverId);
if (server == null) if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
return new ServerStatusResponse() var result = new ServerStatusResponse()
{ {
State = (ServerState)server.State State = (ServerState)server.State
}; };
return Task.FromResult(result);
} }
[HttpGet("{serverId:int}/logs")] [HttpGet("{serverId:int}/logs")]

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Helpers;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
@@ -6,6 +7,7 @@ namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
// This controller hosts endpoints for the statistics for the daemon application itself // This controller hosts endpoints for the statistics for the daemon application itself
[Authorize]
[ApiController] [ApiController]
[Route("api/statistics/application")] [Route("api/statistics/application")]
public class StatisticsApplicationController : Controller public class StatisticsApplicationController : Controller

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services; using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
@@ -6,6 +7,7 @@ namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
// This controller hosts endpoints for the statistics for the docker environment // This controller hosts endpoints for the statistics for the docker environment
[Authorize]
[ApiController] [ApiController]
[Route("api/statistics/docker")] [Route("api/statistics/docker")]
public class StatisticsDockerController : Controller public class StatisticsDockerController : Controller

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Helpers;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
@@ -6,6 +7,7 @@ namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
// This controller hosts endpoints for the statistics for host system the daemon runs on // This controller hosts endpoints for the statistics for host system the daemon runs on
[Authorize]
[ApiController] [ApiController]
[Route("api/statistics/host")] [Route("api/statistics/host")]
public class StatisticsHostController : Controller public class StatisticsHostController : Controller

View File

@@ -1,10 +1,12 @@
using System.Diagnostics; using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services; using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Sys; using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Sys;
namespace MoonlightServers.Daemon.Http.Controllers.Sys; namespace MoonlightServers.Daemon.Http.Controllers.Sys;
[Authorize]
[ApiController] [ApiController]
[Route("api/system/status")] [Route("api/system/status")]
public class SystemStatusController : Controller public class SystemStatusController : Controller

View File

@@ -31,6 +31,13 @@ public class RemoteService
); );
} }
public async Task<ServerDataResponse> GetServer(int serverId)
{
return await ApiClient.GetJson<ServerDataResponse>(
$"api/remote/servers/{serverId}"
);
}
public async Task<ServerInstallDataResponse> GetServerInstallation(int serverId) public async Task<ServerInstallDataResponse> GetServerInstallation(int serverId)
{ {
return await ApiClient.GetJson<ServerInstallDataResponse>( return await ApiClient.GetJson<ServerInstallDataResponse>(

View File

@@ -1,8 +1,11 @@
using Docker.DotNet; using Docker.DotNet;
using Docker.DotNet.Models; using Docker.DotNet.Models;
using MoonCore.Attributes; using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Models; using MoonCore.Models;
using MoonlightServers.Daemon.Abstractions; using MoonlightServers.Daemon.Abstractions;
using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Extensions;
using MoonlightServers.Daemon.Models.Cache; using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses; using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
@@ -16,11 +19,15 @@ public class ServerService : IHostedLifecycleService
private readonly RemoteService RemoteService; private readonly RemoteService RemoteService;
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private bool IsInitialized = false;
private CancellationTokenSource Cancellation = new(); private CancellationTokenSource Cancellation = new();
private bool IsInitialized = false;
public ServerService(RemoteService remoteService, ILogger<ServerService> logger, IServiceProvider serviceProvider, public ServerService(
ILoggerFactory loggerFactory) RemoteService remoteService,
ILogger<ServerService> logger,
IServiceProvider serviceProvider,
ILoggerFactory loggerFactory
)
{ {
RemoteService = remoteService; RemoteService = remoteService;
Logger = logger; Logger = logger;
@@ -35,8 +42,8 @@ public class ServerService : IHostedLifecycleService
Logger.LogWarning("Ignoring initialize call: Already initialized"); Logger.LogWarning("Ignoring initialize call: Already initialized");
return; return;
} }
else
IsInitialized = true; IsInitialized = true;
// Loading models and converting them // Loading models and converting them
Logger.LogInformation("Fetching servers from panel"); Logger.LogInformation("Fetching servers from panel");
@@ -45,25 +52,9 @@ public class ServerService : IHostedLifecycleService
await RemoteService.GetServers(page, pageSize) await RemoteService.GetServers(page, pageSize)
); );
var configurations = servers.Select(x => new ServerConfiguration() var configurations = servers
{ .Select(x => x.ToServerConfiguration())
Id = x.Id, .ToArray();
StartupCommand = x.StartupCommand,
Allocations = x.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
{
IpAddress = y.IpAddress,
Port = y.Port
}).ToArray(),
Variables = x.Variables,
OnlineDetection = x.OnlineDetection,
DockerImage = x.DockerImage,
UseVirtualDisk = x.UseVirtualDisk,
Bandwidth = x.Bandwidth,
Cpu = x.Cpu,
Disk = x.Disk,
Memory = x.Memory,
StopCommand = x.StopCommand
}).ToArray();
Logger.LogInformation("Initializing {count} servers", servers.Length); Logger.LogInformation("Initializing {count} servers", servers.Length);
@@ -91,7 +82,7 @@ public class ServerService : IHostedLifecycleService
await Cancellation.CancelAsync(); await Cancellation.CancelAsync();
} }
private async Task AttachToDockerEvents() private Task AttachToDockerEvents()
{ {
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>(); var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
@@ -123,11 +114,11 @@ public class ServerService : IHostedLifecycleService
await server.NotifyRuntimeContainerDied(); await server.NotifyRuntimeContainerDied();
return; return;
} }
// Check if it's an installation container // Check if it's an installation container
lock (Servers) lock (Servers)
server = Servers.FirstOrDefault(x => x.InstallationContainerId == message.ID); server = Servers.FirstOrDefault(x => x.InstallationContainerId == message.ID);
if (server != null) if (server != null)
{ {
await server.NotifyInstallationContainerDied(); await server.NotifyInstallationContainerDied();
@@ -144,9 +135,11 @@ public class ServerService : IHostedLifecycleService
} }
} }
}); });
return Task.CompletedTask;
} }
private async Task InitializeServerRange(ServerConfiguration[] serverConfigurations) public async Task InitializeServerRange(ServerConfiguration[] serverConfigurations)
{ {
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>(); var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
@@ -173,7 +166,7 @@ public class ServerService : IHostedLifecycleService
await InitializeServer(configuration, existingContainers); await InitializeServer(configuration, existingContainers);
} }
private async Task InitializeServer( public async Task<Server> InitializeServer(
ServerConfiguration serverConfiguration, ServerConfiguration serverConfiguration,
IList<ContainerListResponse> existingContainers IList<ContainerListResponse> existingContainers
) )
@@ -190,6 +183,70 @@ public class ServerService : IHostedLifecycleService
lock (Servers) lock (Servers)
Servers.Add(server); Servers.Add(server);
return server;
}
public async Task Sync(int serverId)
{
var serverData = await RemoteService.GetServer(serverId);
var serverConfiguration = serverData.ToServerConfiguration();
var server = GetServer(serverId);
if (server == null)
await InitializeServer(serverConfiguration, []);
else
server.UpdateConfiguration(serverConfiguration);
}
public async Task Delete(int serverId)
{
var server = GetServer(serverId);
// If a server with this id doesn't exist we can just exit
if(server == null)
return;
if (server.State == ServerState.Installing)
throw new HttpApiException("Unable to delete a server while it is installing", 400);
#region Callbacks
var deleteCompletion = new TaskCompletionSource();
async Task HandleStateChange(ServerState state)
{
if (state == ServerState.Offline)
await DeleteServer();
}
async Task DeleteServer()
{
await server.CancelTasks();
await server.RemoveInstallationVolume();
await server.RemoveRuntimeVolume();
deleteCompletion.SetResult();
lock (Servers)
Servers.Remove(server);
}
#endregion
// If the server is still online, we are killing it and then
// waiting for the callback to trigger notifying us that the server is now offline
// so we can delete it. The request will pause until then using the deleteCompletion task
if (server.State != ServerState.Offline)
{
server.OnStateChanged += HandleStateChange;
await server.Kill();
await deleteCompletion.Task;
}
else
await DeleteServer();
} }
public Server? GetServer(int id) public Server? GetServer(int id)
@@ -197,7 +254,7 @@ public class ServerService : IHostedLifecycleService
lock (Servers) lock (Servers)
return Servers.FirstOrDefault(x => x.Id == id); return Servers.FirstOrDefault(x => x.Id == id);
} }
#region Lifecycle #region Lifecycle
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)

View File

@@ -1,5 +1,8 @@
using System.Text;
using System.Text.Json; using System.Text.Json;
using Docker.DotNet; using Docker.DotNet;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Configuration; using MoonCore.Configuration;
using MoonCore.EnvConfiguration; using MoonCore.EnvConfiguration;
using MoonCore.Extended.Extensions; using MoonCore.Extended.Extensions;
@@ -41,6 +44,7 @@ public class Startup
await RegisterAppConfiguration(); await RegisterAppConfiguration();
await RegisterLogging(); await RegisterLogging();
await RegisterBase(); await RegisterBase();
await RegisterAuth();
await RegisterDocker(); await RegisterDocker();
await RegisterServers(); await RegisterServers();
await RegisterSignalR(); await RegisterSignalR();
@@ -49,6 +53,7 @@ public class Startup
await BuildWebApplication(); await BuildWebApplication();
await UseBase(); await UseBase();
await UseAuth();
await UseCors(); await UseCors();
await UseBaseMiddleware(); await UseBaseMiddleware();
@@ -289,4 +294,40 @@ public class Startup
} }
#endregion #endregion
#region Authentication
private Task RegisterAuth()
{
WebApplicationBuilder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
Configuration.Security.Token
)),
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuer = false,
ClockSkew = TimeSpan.Zero
};
});
WebApplicationBuilder.Services.AddAuthorization();
return Task.CompletedTask;
}
private Task UseAuth()
{
WebApplication.UseAuthentication();
WebApplication.UseAuthorization();
return Task.CompletedTask;
}
#endregion
} }

View File

@@ -0,0 +1,84 @@
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.Shared.Http.Responses.Users.Servers;
namespace MoonlightServers.Frontend.Services;
[Scoped]
public class ServerService
{
private readonly HttpApiClient HttpApiClient;
public ServerService(HttpApiClient httpApiClient)
{
HttpApiClient = httpApiClient;
}
public async Task<PagedData<ServerDetailResponse>> GetServers(int page, int perPage)
{
return await HttpApiClient.GetJson<PagedData<ServerDetailResponse>>(
$"api/client/servers?page={page}&pageSize={perPage}"
);
}
public async Task<ServerDetailResponse> GetServer(int serverId)
{
return await HttpApiClient.GetJson<ServerDetailResponse>(
$"api/client/servers/{serverId}"
);
}
public async Task<ServerStatusResponse> GetStatus(int serverId)
{
return await HttpApiClient.GetJson<ServerStatusResponse>(
$"api/client/servers/{serverId}/status"
);
}
public async Task<ServerLogsResponse> GetLogs(int serverId)
{
return await HttpApiClient.GetJson<ServerLogsResponse>(
$"api/client/servers/{serverId}/logs"
);
}
public async Task<ServerWebSocketResponse> GetWebSocket(int serverId)
{
return await HttpApiClient.GetJson<ServerWebSocketResponse>(
$"api/client/servers/{serverId}/ws"
);
}
public async Task Install(int serverId)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/install"
);
}
#region Power actions
public async Task Start(int serverId)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/start"
);
}
public async Task Stop(int serverId)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/stop"
);
}
public async Task Kill(int serverId)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/kill"
);
}
#endregion
}

View File

@@ -1,8 +1,8 @@
@using MoonCore.Helpers @using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Enums @using MoonlightServers.Shared.Enums
@using MoonlightServers.Shared.Http.Responses.Users.Servers @using MoonlightServers.Shared.Http.Responses.Users.Servers
@inject HttpApiClient ApiClient @inject ServerService ServerService
@inject ILogger<ServerCard> Logger @inject ILogger<ServerCard> Logger
@{ @{
@@ -158,9 +158,7 @@
try try
{ {
Status = await ApiClient.GetJson<ServerStatusResponse>( Status = await ServerService.GetStatus(Server.Id);
$"api/client/servers/{Server.Id}/status"
);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -1,11 +1,12 @@
@using MoonCore.Blazor.Tailwind.Alerts @using MoonCore.Blazor.Tailwind.Alerts
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonCore.Blazor.Tailwind.Components @using MoonCore.Blazor.Tailwind.Components
@using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Enums @using MoonlightServers.Shared.Enums
@inherits BaseServerTab @inherits BaseServerTab
@inject HttpApiClient HttpApiClient @inject ServerService ServerService
@inject AlertService AlertService @inject AlertService AlertService
<div class="grid grid-cols-1 md:col-span-2 lg:grid-cols-3"> <div class="grid grid-cols-1 md:col-span-2 lg:grid-cols-3">
@@ -34,7 +35,7 @@
await AlertService.ConfirmDanger( await AlertService.ConfirmDanger(
"Server installation", "Server installation",
"Do you really want to reinstall the server? This can potentially lead to loss of data", "Do you really want to reinstall the server? This can potentially lead to loss of data",
() => HttpApiClient.Post($"api/client/servers/{Server.Id}/install") () => ServerService.Install(Server.Id)
); );
} }
} }

View File

@@ -1,12 +1,12 @@
@page "/servers" @page "/servers"
@using MoonCore.Helpers
@using MoonlightServers.Frontend.UI.Components.Servers @using MoonlightServers.Frontend.UI.Components.Servers
@using MoonCore.Blazor.Tailwind.Components @using MoonCore.Blazor.Tailwind.Components
@using MoonCore.Models @using MoonCore.Models
@using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Http.Responses.Users.Servers @using MoonlightServers.Shared.Http.Responses.Users.Servers
@inject HttpApiClient ApiClient @inject ServerService ServerService
<LazyLoader Load="Load"> <LazyLoader Load="Load">
<div class="flex flex-col gap-y-5"> <div class="flex flex-col gap-y-5">
@@ -42,9 +42,7 @@
private async Task Load(LazyLoader lazyLoader) private async Task Load(LazyLoader lazyLoader)
{ {
Servers = await PagedData<ServerDetailResponse>.All(async (page, pageSize) => Servers = await PagedData<ServerDetailResponse>.All(async (page, pageSize) =>
await ApiClient.GetJson<PagedData<ServerDetailResponse>>( await ServerService.GetServers(page, pageSize)
$"api/client/servers?page={page}&pageSize={pageSize}"
)
); );
} }
} }

View File

@@ -7,11 +7,12 @@
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonlightServers.Frontend.Interfaces @using MoonlightServers.Frontend.Interfaces
@using MoonlightServers.Frontend.Models @using MoonlightServers.Frontend.Models
@using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Enums @using MoonlightServers.Shared.Enums
@using MoonlightServers.Frontend.UI.Components @using MoonlightServers.Frontend.UI.Components
@using MoonlightServers.Frontend.UI.Components.Servers.ServerTabs @using MoonlightServers.Frontend.UI.Components.Servers.ServerTabs
@inject HttpApiClient ApiClient @inject ServerService ServerService
@inject IEnumerable<IServerTabProvider> TabProviders @inject IEnumerable<IServerTabProvider> TabProviders
@implements IAsyncDisposable @implements IAsyncDisposable
@@ -164,25 +165,19 @@
try try
{ {
// Load meta data // Load meta data
Server = await ApiClient.GetJson<ServerDetailResponse>( Server = await ServerService.GetServer(ServerId);
$"api/client/servers/{ServerId}"
);
// Load server tabs // Load server tabs
foreach (var serverTabProvider in TabProviders) foreach (var serverTabProvider in TabProviders)
Tabs.AddRange(await serverTabProvider.GetTabs(Server)); Tabs.AddRange(await serverTabProvider.GetTabs(Server));
// Load initial status for first render // Load initial status for first render
var status = await ApiClient.GetJson<ServerStatusResponse>( var status = await ServerService.GetStatus(ServerId);
$"api/client/servers/{ServerId}/status"
);
State = status.State; State = status.State;
// Load initial messages // Load initial messages
var initialLogs = await ApiClient.GetJson<ServerLogsResponse>( var initialLogs = await ServerService.GetLogs(ServerId);
$"api/client/servers/{ServerId}/logs"
);
InitialConsoleMessage = ""; InitialConsoleMessage = "";
@@ -190,9 +185,7 @@
InitialConsoleMessage += message; InitialConsoleMessage += message;
// Load websocket meta // Load websocket meta
var websocketDetails = await ApiClient.GetJson<ServerWebSocketResponse>( var websocketDetails = await ServerService.GetWebSocket(ServerId);
$"api/client/servers/{ServerId}/ws"
);
// Build signal r // Build signal r
HubConnection = new HubConnectionBuilder() HubConnection = new HubConnectionBuilder()
@@ -232,13 +225,13 @@
} }
private async Task Start() private async Task Start()
=> await ApiClient.Post($"api/client/servers/{Server.Id}/start"); => await ServerService.Start(ServerId);
private async Task Stop() private async Task Stop()
=> await ApiClient.Post($"api/client/servers/{Server.Id}/stop"); => await ServerService.Stop(ServerId);
private async Task Kill() private async Task Kill()
=> await ApiClient.Post($"api/client/servers/{Server.Id}/kill"); => await ServerService.Kill(ServerId);
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {

View File

@@ -24,8 +24,8 @@ public class CreateServerRequest
public int Bandwidth { get; set; } public int Bandwidth { get; set; }
public string? StartupOverride { get; set; } public string? StartupOverride { get; set; }
public int DockerImageIndex { get; set; } public int DockerImageIndex { get; set; } = -1;
public int StarId { get; set; } public int StarId { get; set; }