Compare commits
2 Commits
2d1b48b0d4
...
e7b1e77d0a
| Author | SHA1 | Date | |
|---|---|---|---|
| e7b1e77d0a | |||
| 7c5dc657dc |
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.3"/>
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -9,15 +9,6 @@
|
|||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": false,
|
|
||||||
"applicationUrl": "https://localhost:7240;http://localhost:5031",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.3" PrivateAssets="all"/>
|
||||||
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
|
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
|
||||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
|
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
|
|
||||||
@source "../bin/Moonlight.Frontend/*.map";
|
@source "../bin/Moonlight.Frontend/*.map";
|
||||||
|
|
||||||
@source "../../../Moonlight.Api/**/*.razor";
|
@source "../../../MoonlightServers.Api/**/*.razor";
|
||||||
@source "../../../Moonlight.Api/**/*.cs";
|
@source "../../../MoonlightServers.Api/**/*.cs";
|
||||||
@source "../../../Moonlight.Api/**/*.html";
|
@source "../../../MoonlightServers.Api/**/*.html";
|
||||||
|
|
||||||
@source "../../../Moonlight.Frontend/**/*.razor";
|
@source "../../../MoonlightServers.Frontend/**/*.razor";
|
||||||
@source "../../../Moonlight.Frontend/**/*.cs";
|
@source "../../../MoonlightServers.Frontend/**/*.cs";
|
||||||
@source "../../../Moonlight.Frontend/**/*.html";
|
@source "../../../MoonlightServers.Frontend/**/*.html";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|||||||
158
MoonlightServers.Api/Admin/Nodes/CrudController.cs
Normal file
158
MoonlightServers.Api/Admin/Nodes/CrudController.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
|
using Moonlight.Shared.Http.Requests;
|
||||||
|
using Moonlight.Shared.Http.Responses;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||||
|
using MoonlightServers.Shared;
|
||||||
|
using MoonlightServers.Shared.Admin.Nodes;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Nodes;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/servers/nodes")]
|
||||||
|
public class CrudController : Controller
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<Node> DatabaseRepository;
|
||||||
|
private readonly HybridCache Cache;
|
||||||
|
|
||||||
|
public CrudController(
|
||||||
|
DatabaseRepository<Node> databaseRepository,
|
||||||
|
HybridCache cache
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DatabaseRepository = databaseRepository;
|
||||||
|
Cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize(Policy = Permissions.Nodes.View)]
|
||||||
|
public async Task<ActionResult<PagedData<NodeDto>>> GetAsync(
|
||||||
|
[FromQuery] int startIndex,
|
||||||
|
[FromQuery] int length,
|
||||||
|
[FromQuery] FilterOptions? filterOptions
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Validation
|
||||||
|
if (startIndex < 0)
|
||||||
|
return Problem("Invalid start index specified", statusCode: 400);
|
||||||
|
|
||||||
|
if (length is < 1 or > 100)
|
||||||
|
return Problem("Invalid length specified");
|
||||||
|
|
||||||
|
// Query building
|
||||||
|
|
||||||
|
var query = DatabaseRepository
|
||||||
|
.Query();
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
if (filterOptions != null)
|
||||||
|
{
|
||||||
|
foreach (var filterOption in filterOptions.Filters)
|
||||||
|
{
|
||||||
|
query = filterOption.Key switch
|
||||||
|
{
|
||||||
|
nameof(Node.Name) =>
|
||||||
|
query.Where(role => EF.Functions.ILike(role.Name, $"%{filterOption.Value}%")),
|
||||||
|
|
||||||
|
_ => query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
var data = await query
|
||||||
|
.OrderBy(x => x.Id)
|
||||||
|
.ProjectToDto()
|
||||||
|
.Skip(startIndex)
|
||||||
|
.Take(length)
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
|
||||||
|
return new PagedData<NodeDto>(data, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Nodes.View)]
|
||||||
|
public async Task<ActionResult<NodeDto>> GetAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var node = await DatabaseRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (node == null)
|
||||||
|
return Problem("No node with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
return NodeMapper.ToDto(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Policy = Permissions.Nodes.Create)]
|
||||||
|
public async Task<ActionResult<NodeDto>> CreateAsync([FromBody] CreateNodeDto request)
|
||||||
|
{
|
||||||
|
var node = NodeMapper.ToEntity(request);
|
||||||
|
|
||||||
|
node.TokenId = GenerateString(10);
|
||||||
|
node.Token = GenerateString(64);
|
||||||
|
|
||||||
|
var finalRole = await DatabaseRepository.AddAsync(node);
|
||||||
|
|
||||||
|
return NodeMapper.ToDto(finalRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Nodes.Edit)]
|
||||||
|
public async Task<ActionResult<NodeDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateNodeDto request)
|
||||||
|
{
|
||||||
|
var node = await DatabaseRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (node == null)
|
||||||
|
return Problem("No node with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
NodeMapper.Merge(node, request);
|
||||||
|
|
||||||
|
await DatabaseRepository.UpdateAsync(node);
|
||||||
|
|
||||||
|
return NodeMapper.ToDto(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Nodes.Delete)]
|
||||||
|
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var node = await DatabaseRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (node == null)
|
||||||
|
return Problem("No node with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
await DatabaseRepository.RemoveAsync(node);
|
||||||
|
|
||||||
|
// Remove cache for node token auth scheme
|
||||||
|
await Cache.RemoveAsync(string.Format(NodeTokenSchemeHandler.CacheKeyFormat, node.TokenId));
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateString(int length)
|
||||||
|
{
|
||||||
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
var stringBuilder = new StringBuilder();
|
||||||
|
var random = new Random();
|
||||||
|
|
||||||
|
for (var i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
stringBuilder.Append(chars[random.Next(chars.Length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
51
MoonlightServers.Api/Admin/Nodes/HealthController.cs
Normal file
51
MoonlightServers.Api/Admin/Nodes/HealthController.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Shared;
|
||||||
|
using MoonlightServers.Shared.Admin.Nodes;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Nodes;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Policy = Permissions.Nodes.View)]
|
||||||
|
[Route("api/admin/servers/nodes/{id:int}/health")]
|
||||||
|
public class HealthController : Controller
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<Node> DatabaseRepository;
|
||||||
|
private readonly NodeService NodeService;
|
||||||
|
private readonly ILogger<HealthController> Logger;
|
||||||
|
|
||||||
|
public HealthController(
|
||||||
|
DatabaseRepository<Node> databaseRepository,
|
||||||
|
NodeService nodeService,
|
||||||
|
ILogger<HealthController> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DatabaseRepository = databaseRepository;
|
||||||
|
NodeService = nodeService;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<NodeHealthDto>> GetAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var node = await DatabaseRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (node == null)
|
||||||
|
return Problem("No node with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
var health = await NodeService.GetHealthAsync(node);
|
||||||
|
|
||||||
|
return new NodeHealthDto()
|
||||||
|
{
|
||||||
|
StatusCode = health.StatusCode,
|
||||||
|
RemoteStatusCode = health.Dto?.RemoteStatusCode ?? 0,
|
||||||
|
IsHealthy = health is { StatusCode: >= 200 and <= 299, Dto.RemoteStatusCode: >= 200 and <= 299 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
17
MoonlightServers.Api/Admin/Nodes/NodeMapper.cs
Normal file
17
MoonlightServers.Api/Admin/Nodes/NodeMapper.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Shared.Admin.Nodes;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Nodes;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||||
|
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||||
|
public static partial class NodeMapper
|
||||||
|
{
|
||||||
|
public static partial NodeDto ToDto(Node node);
|
||||||
|
public static partial IQueryable<NodeDto> ProjectToDto(this IQueryable<Node> nodes);
|
||||||
|
public static partial Node ToEntity(CreateNodeDto dto);
|
||||||
|
public static partial void Merge([MappingTarget] Node node, UpdateNodeDto dto);
|
||||||
|
}
|
||||||
132
MoonlightServers.Api/Admin/Nodes/NodeService.cs
Normal file
132
MoonlightServers.Api/Admin/Nodes/NodeService.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.DaemonShared.Http;
|
||||||
|
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Nodes;
|
||||||
|
|
||||||
|
public class NodeService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory ClientFactory;
|
||||||
|
private readonly ILogger<NodeService> Logger;
|
||||||
|
public NodeService(IHttpClientFactory clientFactory, ILogger<NodeService> logger)
|
||||||
|
{
|
||||||
|
ClientFactory = clientFactory;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NodeHealthStatus> GetHealthAsync(Node node)
|
||||||
|
{
|
||||||
|
var client = ClientFactory.CreateClient();
|
||||||
|
|
||||||
|
var request = CreateBaseRequest(node, HttpMethod.Get, "api/health");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return new NodeHealthStatus((int)response.StatusCode, null);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var health = await response
|
||||||
|
.Content
|
||||||
|
.ReadFromJsonAsync<HealthDto>(SerializationContext.Default.Options);
|
||||||
|
|
||||||
|
return new NodeHealthStatus((int)response.StatusCode, health);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(e, "An unhandled error occured while processing health response of node {id}", node.Id);
|
||||||
|
|
||||||
|
return new NodeHealthStatus((int)response.StatusCode, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(e, "An error occured while fetching health status of node {id}", node.Id);
|
||||||
|
return new NodeHealthStatus(0, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpRequestMessage CreateBaseRequest(
|
||||||
|
Node node,
|
||||||
|
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||||
|
HttpMethod method,
|
||||||
|
string endpoint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage();
|
||||||
|
|
||||||
|
request.Headers.Add(HeaderNames.Authorization, node.Token);
|
||||||
|
request.RequestUri = new Uri(new Uri(node.HttpEndpointUrl), 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 NodeException(
|
||||||
|
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 record NodeHealthStatus(int StatusCode, HealthDto? Dto);
|
||||||
|
|
||||||
|
public class NodeException : Exception
|
||||||
|
{
|
||||||
|
public string Type { get; }
|
||||||
|
public string Title { get; }
|
||||||
|
public int Status { get; }
|
||||||
|
public string? Detail { get; }
|
||||||
|
public Dictionary<string, string[]>? Errors { get; }
|
||||||
|
|
||||||
|
public NodeException(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
173
MoonlightServers.Api/Admin/Templates/CrudController.cs
Normal file
173
MoonlightServers.Api/Admin/Templates/CrudController.cs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Shared.Http.Requests;
|
||||||
|
using Moonlight.Shared.Http.Responses;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
using MoonlightServers.Shared;
|
||||||
|
using MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/servers/templates")]
|
||||||
|
public class CrudController : Controller
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<Template> DatabaseRepository;
|
||||||
|
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||||
|
|
||||||
|
public CrudController(
|
||||||
|
DatabaseRepository<Template> databaseRepository,
|
||||||
|
DatabaseRepository<TemplateDockerImage> dockerImageRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DatabaseRepository = databaseRepository;
|
||||||
|
DockerImageRepository = dockerImageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize(Policy = Permissions.Templates.View)]
|
||||||
|
public async Task<ActionResult<PagedData<TemplateDto>>> GetAsync(
|
||||||
|
[FromQuery] int startIndex,
|
||||||
|
[FromQuery] int length,
|
||||||
|
[FromQuery] FilterOptions? filterOptions
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Validation
|
||||||
|
if (startIndex < 0)
|
||||||
|
return Problem("Invalid start index specified", statusCode: 400);
|
||||||
|
|
||||||
|
if (length is < 1 or > 100)
|
||||||
|
return Problem("Invalid length specified");
|
||||||
|
|
||||||
|
// Query building
|
||||||
|
|
||||||
|
var query = DatabaseRepository
|
||||||
|
.Query();
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
if (filterOptions != null)
|
||||||
|
{
|
||||||
|
foreach (var filterOption in filterOptions.Filters)
|
||||||
|
{
|
||||||
|
query = filterOption.Key switch
|
||||||
|
{
|
||||||
|
nameof(Template.Name) =>
|
||||||
|
query.Where(role => EF.Functions.ILike(role.Name, $"%{filterOption.Value}%")),
|
||||||
|
|
||||||
|
_ => query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
var data = await query
|
||||||
|
.OrderBy(x => x.Id)
|
||||||
|
.ProjectToDto()
|
||||||
|
.Skip(startIndex)
|
||||||
|
.Take(length)
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
|
||||||
|
return new PagedData<TemplateDto>(data, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.View)]
|
||||||
|
public async Task<ActionResult<DetailedTemplateDto>> GetAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var template = await DatabaseRepository
|
||||||
|
.Query()
|
||||||
|
.Include(x => x.DefaultDockerImage)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (template == null)
|
||||||
|
return Problem("No template with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDetailedDto(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Create)]
|
||||||
|
public async Task<ActionResult<TemplateDto>> CreateAsync([FromBody] CreateTemplateDto request)
|
||||||
|
{
|
||||||
|
var template = TemplateMapper.ToEntity(request);
|
||||||
|
|
||||||
|
// Fill in default values
|
||||||
|
template.LifecycleConfig = new()
|
||||||
|
{
|
||||||
|
StartupCommands = [
|
||||||
|
new StartupCommand
|
||||||
|
{
|
||||||
|
DisplayName = "Default Startup",
|
||||||
|
Command = "bash startup.sh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
StopCommand = "^C",
|
||||||
|
OnlineLogPatterns = ["I am online"]
|
||||||
|
};
|
||||||
|
|
||||||
|
template.InstallationConfig = new()
|
||||||
|
{
|
||||||
|
DockerImage = "debian",
|
||||||
|
Script = "#!/bin/bash\necho Installing",
|
||||||
|
Shell = "/bin/bash"
|
||||||
|
};
|
||||||
|
|
||||||
|
template.FilesConfig = new()
|
||||||
|
{
|
||||||
|
ConfigurationFiles = []
|
||||||
|
};
|
||||||
|
|
||||||
|
template.MiscellaneousConfig = new()
|
||||||
|
{
|
||||||
|
UseLegacyStartup = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalRole = await DatabaseRepository.AddAsync(template);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(finalRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Edit)]
|
||||||
|
public async Task<ActionResult<DetailedTemplateDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateTemplateDto request)
|
||||||
|
{
|
||||||
|
var template = await DatabaseRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (template == null)
|
||||||
|
return Problem("No template with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
TemplateMapper.Merge(template, request);
|
||||||
|
|
||||||
|
template.DefaultDockerImage = await DockerImageRepository
|
||||||
|
.Query()
|
||||||
|
.Where(x => x.Template.Id == id)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.DefaultDockerImageId);
|
||||||
|
|
||||||
|
await DatabaseRepository.UpdateAsync(template);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDetailedDto(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Delete)]
|
||||||
|
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var template = await DatabaseRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (template == null)
|
||||||
|
return Problem("No template with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
await DatabaseRepository.RemoveAsync(template);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
144
MoonlightServers.Api/Admin/Templates/DockerImagesController.cs
Normal file
144
MoonlightServers.Api/Admin/Templates/DockerImagesController.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Shared.Http.Responses;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Shared;
|
||||||
|
using MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/servers/templates/{templateId:int}/dockerImages")]
|
||||||
|
public class DockerImagesController : Controller
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||||
|
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||||
|
|
||||||
|
public DockerImagesController(
|
||||||
|
DatabaseRepository<TemplateDockerImage> dockerImageRepository,
|
||||||
|
DatabaseRepository<Template> templateRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DockerImageRepository = dockerImageRepository;
|
||||||
|
TemplateRepository = templateRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize(Policy = Permissions.Templates.View)]
|
||||||
|
public async Task<ActionResult<PagedData<DockerImageDto>>> GetAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromQuery] int startIndex,
|
||||||
|
[FromQuery] int length
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Validation
|
||||||
|
if (startIndex < 0)
|
||||||
|
return Problem("Invalid start index specified", statusCode: 400);
|
||||||
|
|
||||||
|
if (length is < 1 or > 100)
|
||||||
|
return Problem("Invalid length specified");
|
||||||
|
|
||||||
|
if (!await TemplateRepository.Query().AnyAsync(x => x.Id == templateId))
|
||||||
|
return Problem("No template with that id found", statusCode: 404);
|
||||||
|
|
||||||
|
// Query building
|
||||||
|
|
||||||
|
var query = DockerImageRepository
|
||||||
|
.Query()
|
||||||
|
.Where(x => x.Template.Id == templateId);
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
var data = await query
|
||||||
|
.OrderBy(x => x.Id)
|
||||||
|
.ProjectToDto()
|
||||||
|
.Skip(startIndex)
|
||||||
|
.Take(length)
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
|
||||||
|
return new PagedData<DockerImageDto>(data, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.View)]
|
||||||
|
public async Task<ActionResult<DockerImageDto>> GetAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromRoute] int id
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var templateDockerImage = await DockerImageRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||||
|
|
||||||
|
if (templateDockerImage == null)
|
||||||
|
return Problem("No template or template dockerImage found with that id");
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(templateDockerImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Create)]
|
||||||
|
public async Task<ActionResult<DockerImageDto>> CreateAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromBody] CreateDockerImageDto dto
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var template = await TemplateRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == templateId);
|
||||||
|
|
||||||
|
if (template == null)
|
||||||
|
return Problem("No template with that id found", statusCode: 404);
|
||||||
|
|
||||||
|
var dockerImage = TemplateMapper.ToEntity(dto);
|
||||||
|
|
||||||
|
dockerImage.Template = template;
|
||||||
|
|
||||||
|
var finalDockerImage = await DockerImageRepository.AddAsync(dockerImage);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(finalDockerImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Edit)]
|
||||||
|
public async Task<ActionResult<DockerImageDto>> UpdateAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromRoute] int id,
|
||||||
|
[FromBody] UpdateDockerImageDto dto
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var templateDockerImage = await DockerImageRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||||
|
|
||||||
|
if (templateDockerImage == null)
|
||||||
|
return Problem("No template or template dockerImage found with that id");
|
||||||
|
|
||||||
|
TemplateMapper.Merge(templateDockerImage, dto);
|
||||||
|
|
||||||
|
await DockerImageRepository.UpdateAsync(templateDockerImage);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(templateDockerImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Delete)]
|
||||||
|
public async Task<ActionResult> DeleteAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromRoute] int id
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var templateDockerImage = await DockerImageRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||||
|
|
||||||
|
if (templateDockerImage == null)
|
||||||
|
return Problem("No template or template dockerImage found with that id");
|
||||||
|
|
||||||
|
await DockerImageRepository.RemoveAsync(templateDockerImage);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
207
MoonlightServers.Api/Admin/Templates/PelicanEggImportService.cs
Normal file
207
MoonlightServers.Api/Admin/Templates/PelicanEggImportService.cs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
using System.Text;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
using VYaml.Annotations;
|
||||||
|
using VYaml.Serialization;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
public class PelicanEggImportService
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||||
|
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||||
|
|
||||||
|
public PelicanEggImportService(
|
||||||
|
DatabaseRepository<Template> templateRepository,
|
||||||
|
DatabaseRepository<TemplateDockerImage> dockerImageRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
TemplateRepository = templateRepository;
|
||||||
|
DockerImageRepository = dockerImageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Template> ImportAsync(string content)
|
||||||
|
{
|
||||||
|
var egg = YamlSerializer.Deserialize<Egg>(Encoding.UTF8.GetBytes(content));
|
||||||
|
|
||||||
|
var template = new Template()
|
||||||
|
{
|
||||||
|
AllowUserDockerImageChange = true,
|
||||||
|
Author = egg.Author,
|
||||||
|
Description = egg.Description,
|
||||||
|
DonateUrl = null,
|
||||||
|
Name = egg.Name,
|
||||||
|
UpdateUrl = egg.Meta.UpdateUrl,
|
||||||
|
Version = "1.0.0",
|
||||||
|
FilesConfig = new FilesConfig()
|
||||||
|
{
|
||||||
|
ConfigurationFiles = egg.Config.Files.Select(file => new ConfigurationFile()
|
||||||
|
{
|
||||||
|
Path = file.Key,
|
||||||
|
Parser = file.Value.Parser,
|
||||||
|
Mappings = file.Value.Find.Select(pair => new ConfigurationFileMapping()
|
||||||
|
{
|
||||||
|
Key = pair.Key,
|
||||||
|
Value = pair.Value
|
||||||
|
}).ToList()
|
||||||
|
}).ToList()
|
||||||
|
},
|
||||||
|
InstallationConfig = new InstallationConfig()
|
||||||
|
{
|
||||||
|
DockerImage = egg.Scripts.Installation.Container,
|
||||||
|
Script = egg.Scripts.Installation.Script,
|
||||||
|
Shell = egg.Scripts.Installation.Entrypoint
|
||||||
|
},
|
||||||
|
LifecycleConfig = new LifecycleConfig()
|
||||||
|
{
|
||||||
|
OnlineLogPatterns = egg.Config.Startup.Values.ToList(),
|
||||||
|
StopCommand = egg.Config.Stop,
|
||||||
|
StartupCommands = egg.StartupCommands.Select(x => new StartupCommand()
|
||||||
|
{
|
||||||
|
DisplayName = x.Key,
|
||||||
|
Command = x.Value
|
||||||
|
}).ToList()
|
||||||
|
},
|
||||||
|
MiscellaneousConfig = new MiscellaneousConfig()
|
||||||
|
{
|
||||||
|
UseLegacyStartup = true
|
||||||
|
},
|
||||||
|
Variables = egg.Variables.Select(variable => new TemplateVariable()
|
||||||
|
{
|
||||||
|
Description = variable.Description,
|
||||||
|
DisplayName = variable.Name,
|
||||||
|
DefaultValue = variable.DefaultValue,
|
||||||
|
EnvName = variable.EnvVariable
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalTemplate = await TemplateRepository.AddAsync(template);
|
||||||
|
|
||||||
|
var isFirst = true;
|
||||||
|
TemplateDockerImage? defaultDockerImage = null;
|
||||||
|
|
||||||
|
foreach (var dockerImage in egg.DockerImages)
|
||||||
|
{
|
||||||
|
var finalDockerImage = await DockerImageRepository.AddAsync(new TemplateDockerImage()
|
||||||
|
{
|
||||||
|
DisplayName = dockerImage.Key,
|
||||||
|
ImageName = dockerImage.Value,
|
||||||
|
SkipPulling = false,
|
||||||
|
Template = finalTemplate
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isFirst)
|
||||||
|
{
|
||||||
|
isFirst = false;
|
||||||
|
defaultDockerImage = finalDockerImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalTemplate.DefaultDockerImage = defaultDockerImage;
|
||||||
|
|
||||||
|
await TemplateRepository.UpdateAsync(finalTemplate);
|
||||||
|
|
||||||
|
return finalTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class Egg
|
||||||
|
{
|
||||||
|
[YamlMember("_comment")] public string? Comment { get; set; }
|
||||||
|
|
||||||
|
[YamlMember("meta")] public EggMeta Meta { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("exported_at")] public string? ExportedAt { get; set; }
|
||||||
|
|
||||||
|
[YamlMember("name")] public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("author")] public string Author { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("uuid")] public string Uuid { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("description")] public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("image")] public string? Image { get; set; }
|
||||||
|
|
||||||
|
[YamlMember("tags")] public List<string> Tags { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("features")] public List<string> Features { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("docker_images")] public Dictionary<string, string> DockerImages { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("file_denylist")] public Dictionary<string, string> FileDenylist { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("startup_commands")] public Dictionary<string, string> StartupCommands { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("config")] public EggConfig Config { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("scripts")] public EggScripts Scripts { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("variables")] public List<EggVariable> Variables { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class EggMeta
|
||||||
|
{
|
||||||
|
[YamlMember("version")] public string Version { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("update_url")] public string? UpdateUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class EggConfig
|
||||||
|
{
|
||||||
|
[YamlMember("files")] public Dictionary<string, EggConfigFile> Files { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("startup")] public Dictionary<string, string> Startup { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("logs")] public Dictionary<string, string> Logs { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("stop")] public string Stop { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class EggConfigFile
|
||||||
|
{
|
||||||
|
[YamlMember("parser")] public string Parser { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("find")] public Dictionary<string, string> Find { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class EggScripts
|
||||||
|
{
|
||||||
|
[YamlMember("installation")] public EggInstallationScript Installation { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class EggInstallationScript
|
||||||
|
{
|
||||||
|
[YamlMember("script")] public string Script { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("container")] public string Container { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("entrypoint")] public string Entrypoint { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class EggVariable
|
||||||
|
{
|
||||||
|
[YamlMember("name")] public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("description")] public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("env_variable")] public string EnvVariable { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("default_value")] public string DefaultValue { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[YamlMember("user_viewable")] public bool UserViewable { get; set; }
|
||||||
|
|
||||||
|
[YamlMember("user_editable")] public bool UserEditable { get; set; }
|
||||||
|
|
||||||
|
[YamlMember("rules")] public List<string> Rules { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember("sort")] public int Sort { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
public class PterodactylEggImportService
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||||
|
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||||
|
|
||||||
|
public PterodactylEggImportService(
|
||||||
|
DatabaseRepository<Template> templateRepository,
|
||||||
|
DatabaseRepository<TemplateDockerImage> dockerImageRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
TemplateRepository = templateRepository;
|
||||||
|
DockerImageRepository = dockerImageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Template> ImportAsync(string json)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
var template = new Template
|
||||||
|
{
|
||||||
|
Name = Truncate(root.GetStringOrDefault("name") ?? "Unknown", 30),
|
||||||
|
Description = Truncate(root.GetStringOrDefault("description") ?? "", 255),
|
||||||
|
Author = Truncate(root.GetStringOrDefault("author") ?? "", 30),
|
||||||
|
Version = "1.0.0",
|
||||||
|
UpdateUrl = root.TryGetProperty("meta", out var meta)
|
||||||
|
? meta.GetStringOrDefault("update_url")
|
||||||
|
: null,
|
||||||
|
DonateUrl = null,
|
||||||
|
|
||||||
|
FilesConfig = ParseFilesConfig(root),
|
||||||
|
LifecycleConfig = ParseLifecycleConfig(root),
|
||||||
|
InstallationConfig = ParseInstallationConfig(root),
|
||||||
|
MiscellaneousConfig = new MiscellaneousConfig { UseLegacyStartup = true },
|
||||||
|
|
||||||
|
AllowUserDockerImageChange = true,
|
||||||
|
Variables = ParseVariables(root)
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalTemplate = await TemplateRepository.AddAsync(template);
|
||||||
|
|
||||||
|
var dockerImages = ParseDockerImageModels(root);
|
||||||
|
TemplateDockerImage? defaultDockerImage = null;
|
||||||
|
|
||||||
|
var isFirst = true;
|
||||||
|
|
||||||
|
foreach (var (displayName, imageName) in dockerImages)
|
||||||
|
{
|
||||||
|
var entity = new TemplateDockerImage
|
||||||
|
{
|
||||||
|
DisplayName = displayName,
|
||||||
|
ImageName = imageName,
|
||||||
|
SkipPulling = false,
|
||||||
|
Template = finalTemplate
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalEntity = await DockerImageRepository.AddAsync(entity);
|
||||||
|
|
||||||
|
if (isFirst)
|
||||||
|
{
|
||||||
|
isFirst = false;
|
||||||
|
defaultDockerImage = finalEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalTemplate.DefaultDockerImage = defaultDockerImage;
|
||||||
|
await TemplateRepository.UpdateAsync(finalTemplate);
|
||||||
|
|
||||||
|
return finalTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FilesConfig ParseFilesConfig(JsonElement root)
|
||||||
|
{
|
||||||
|
var configFiles = new List<ConfigurationFile>();
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("config", out var config))
|
||||||
|
return new FilesConfig { ConfigurationFiles = configFiles };
|
||||||
|
|
||||||
|
if (!config.TryGetProperty("files", out var filesElement))
|
||||||
|
return new FilesConfig { ConfigurationFiles = configFiles };
|
||||||
|
|
||||||
|
var filesJson = filesElement.ValueKind == JsonValueKind.String
|
||||||
|
? filesElement.GetString()
|
||||||
|
: filesElement.GetRawText();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(filesJson) || filesJson == "{}" || filesJson == "[]")
|
||||||
|
return new FilesConfig { ConfigurationFiles = configFiles };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var filesDoc = JsonDocument.Parse(filesJson);
|
||||||
|
|
||||||
|
foreach (var fileProperty in filesDoc.RootElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
var parser = fileProperty.Value.GetStringOrDefault("parser") ?? "json";
|
||||||
|
var mappings = new List<ConfigurationFileMapping>();
|
||||||
|
|
||||||
|
if (fileProperty.Value.TryGetProperty("find", out var find))
|
||||||
|
{
|
||||||
|
foreach (var mapping in find.EnumerateObject())
|
||||||
|
{
|
||||||
|
mappings.Add(new ConfigurationFileMapping
|
||||||
|
{
|
||||||
|
Key = mapping.Name,
|
||||||
|
Value = mapping.Value.ValueKind == JsonValueKind.String
|
||||||
|
? mapping.Value.GetString() ?? ""
|
||||||
|
: mapping.Value.GetRawText()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configFiles.Add(new ConfigurationFile
|
||||||
|
{
|
||||||
|
Path = fileProperty.Name,
|
||||||
|
Parser = parser,
|
||||||
|
Mappings = mappings
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilesConfig { ConfigurationFiles = configFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LifecycleConfig ParseLifecycleConfig(JsonElement root)
|
||||||
|
{
|
||||||
|
var stopCommand = "";
|
||||||
|
var onlinePatterns = new List<string>();
|
||||||
|
|
||||||
|
if (root.TryGetProperty("config", out var config))
|
||||||
|
{
|
||||||
|
stopCommand = config.GetStringOrDefault("stop") ?? "";
|
||||||
|
|
||||||
|
if (config.TryGetProperty("startup", out var startupElement))
|
||||||
|
{
|
||||||
|
var startupJson = startupElement.ValueKind == JsonValueKind.String
|
||||||
|
? startupElement.GetString()
|
||||||
|
: startupElement.GetRawText();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(startupJson))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var startupDoc = JsonDocument.Parse(startupJson);
|
||||||
|
|
||||||
|
if (startupDoc.RootElement.TryGetProperty("done", out var done))
|
||||||
|
{
|
||||||
|
var doneValue = done.ValueKind == JsonValueKind.String
|
||||||
|
? done.GetString()
|
||||||
|
: done.GetRawText();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(doneValue))
|
||||||
|
onlinePatterns.Add(doneValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LifecycleConfig
|
||||||
|
{
|
||||||
|
StartupCommands =
|
||||||
|
[
|
||||||
|
new StartupCommand
|
||||||
|
{
|
||||||
|
DisplayName = "Startup",
|
||||||
|
Command = root.GetStringOrDefault("startup") ?? ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
StopCommand = stopCommand,
|
||||||
|
OnlineLogPatterns = onlinePatterns
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InstallationConfig ParseInstallationConfig(JsonElement root)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty("scripts", out var scripts))
|
||||||
|
return new InstallationConfig();
|
||||||
|
|
||||||
|
if (!scripts.TryGetProperty("installation", out var installation))
|
||||||
|
return new InstallationConfig();
|
||||||
|
|
||||||
|
return new InstallationConfig
|
||||||
|
{
|
||||||
|
DockerImage = installation.GetStringOrDefault("container") ?? "",
|
||||||
|
Shell = installation.GetStringOrDefault("entrypoint") ?? "bash",
|
||||||
|
Script = installation.GetStringOrDefault("script") ?? ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns (DisplayName, ImageName, IsFirst) tuples to avoid a temporary model
|
||||||
|
private static List<(string DisplayName, string ImageName)> ParseDockerImageModels(JsonElement root)
|
||||||
|
{
|
||||||
|
var result = new List<(string, string)>();
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("docker_images", out var dockerImages))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
foreach (var img in dockerImages.EnumerateObject())
|
||||||
|
{
|
||||||
|
result.Add((
|
||||||
|
Truncate(img.Name, 30),
|
||||||
|
Truncate(img.Value.GetString() ?? img.Name, 255)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TemplateVariable> ParseVariables(JsonElement root)
|
||||||
|
{
|
||||||
|
var variables = new List<TemplateVariable>();
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("variables", out var vars))
|
||||||
|
return variables;
|
||||||
|
|
||||||
|
foreach (var v in vars.EnumerateArray())
|
||||||
|
{
|
||||||
|
variables.Add(new TemplateVariable
|
||||||
|
{
|
||||||
|
DisplayName = Truncate(v.GetStringOrDefault("name") ?? "Variable", 30),
|
||||||
|
Description = Truncate(v.GetStringOrDefault("description") ?? "", 255),
|
||||||
|
EnvName = Truncate(v.GetStringOrDefault("env_variable") ?? "", 60),
|
||||||
|
DefaultValue = Truncate(v.GetStringOrDefault("default_value") ?? "", 1024)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Truncate(string value, int maxLength) =>
|
||||||
|
value.Length <= maxLength ? value : value[..maxLength];
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class JsonElementExtensions
|
||||||
|
{
|
||||||
|
public static string? GetStringOrDefault(this JsonElement element, string propertyName)
|
||||||
|
{
|
||||||
|
return element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String
|
||||||
|
? prop.GetString()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
MoonlightServers.Api/Admin/Templates/TemplateMapper.cs
Normal file
28
MoonlightServers.Api/Admin/Templates/TemplateMapper.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Shared.Admin.Templates;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||||
|
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||||
|
public static partial class TemplateMapper
|
||||||
|
{
|
||||||
|
public static partial TemplateDto ToDto(Template template);
|
||||||
|
public static partial DetailedTemplateDto ToDetailedDto(Template template);
|
||||||
|
public static partial IQueryable<TemplateDto> ProjectToDto(this IQueryable<Template> templates);
|
||||||
|
public static partial Template ToEntity(CreateTemplateDto dto);
|
||||||
|
public static partial void Merge([MappingTarget] Template template, UpdateTemplateDto dto);
|
||||||
|
|
||||||
|
public static partial IQueryable<VariableDto> ProjectToDto(this IQueryable<TemplateVariable> variables);
|
||||||
|
public static partial VariableDto ToDto(TemplateVariable variable);
|
||||||
|
public static partial TemplateVariable ToEntity(CreateVariableDto dto);
|
||||||
|
public static partial void Merge([MappingTarget] TemplateVariable variable, UpdateVariableDto dto);
|
||||||
|
|
||||||
|
public static partial IQueryable<DockerImageDto> ProjectToDto(this IQueryable<TemplateDockerImage> dockerImages);
|
||||||
|
public static partial DockerImageDto ToDto(TemplateDockerImage dockerImage);
|
||||||
|
public static partial TemplateDockerImage ToEntity(CreateDockerImageDto dto);
|
||||||
|
public static partial void Merge([MappingTarget] TemplateDockerImage dockerImage, UpdateDockerImageDto dto);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using VYaml.Annotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class TemplateTransferModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public string Author { get; set; } = "";
|
||||||
|
public string Version { get; set; } = "";
|
||||||
|
public string? UpdateUrl { get; set; }
|
||||||
|
public string? DonateUrl { get; set; }
|
||||||
|
|
||||||
|
public FilesConfigTransferModel Files { get; set; } = new();
|
||||||
|
public LifecycleConfigTransferModel Lifecycle { get; set; } = new();
|
||||||
|
public InstallationConfigTransferModel Installation { get; set; } = new();
|
||||||
|
public MiscellaneousConfigTransferModel Miscellaneous { get; set; } = new();
|
||||||
|
|
||||||
|
public bool AllowUserDockerImageChange { get; set; }
|
||||||
|
public List<TemplateDockerImageTransferModel> DockerImages { get; set; } = new();
|
||||||
|
|
||||||
|
public List<TemplateVariableTransferModel> Variables { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class TemplateDockerImageTransferModel
|
||||||
|
{
|
||||||
|
public string DisplayName { get; set; } = "";
|
||||||
|
public string ImageName { get; set; } = "";
|
||||||
|
public bool SkipPulling { get; set; }
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class TemplateVariableTransferModel
|
||||||
|
{
|
||||||
|
public string DisplayName { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public string EnvName { get; set; } = "";
|
||||||
|
public string? DefaultValue { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class FilesConfigTransferModel
|
||||||
|
{
|
||||||
|
public List<ConfigurationFileTransferModel> ConfigurationFiles { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class ConfigurationFileTransferModel
|
||||||
|
{
|
||||||
|
public string Path { get; set; } = "";
|
||||||
|
public string Parser { get; set; } = "";
|
||||||
|
public List<ConfigurationFileMappingTransferModel> Mappings { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class ConfigurationFileMappingTransferModel
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = "";
|
||||||
|
public string? Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class LifecycleConfigTransferModel
|
||||||
|
{
|
||||||
|
public List<StartupCommandTransferModel> StartupCommands { get; set; } = new();
|
||||||
|
public string StopCommand { get; set; } = "";
|
||||||
|
public List<string> OnlineLogPatterns { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class StartupCommandTransferModel
|
||||||
|
{
|
||||||
|
public string DisplayName { get; set; } = "";
|
||||||
|
public string Command { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class InstallationConfigTransferModel
|
||||||
|
{
|
||||||
|
public string DockerImage { get; set; } = "";
|
||||||
|
public string Shell { get; set; } = "";
|
||||||
|
public string Script { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class MiscellaneousConfigTransferModel
|
||||||
|
{
|
||||||
|
public bool UseLegacyStartup { get; set; }
|
||||||
|
}
|
||||||
182
MoonlightServers.Api/Admin/Templates/TemplateTransferService.cs
Normal file
182
MoonlightServers.Api/Admin/Templates/TemplateTransferService.cs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
public class TemplateTransferService
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||||
|
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||||
|
|
||||||
|
public TemplateTransferService(DatabaseRepository<Template> templateRepository,
|
||||||
|
DatabaseRepository<TemplateDockerImage> dockerImageRepository)
|
||||||
|
{
|
||||||
|
TemplateRepository = templateRepository;
|
||||||
|
DockerImageRepository = dockerImageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TemplateTransferModel?> ExportAsync(int id)
|
||||||
|
{
|
||||||
|
var template = await TemplateRepository
|
||||||
|
.Query()
|
||||||
|
.Include(x => x.Variables)
|
||||||
|
.Include(x => x.DockerImages)
|
||||||
|
.Include(x => x.DefaultDockerImage)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (template == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Name = template.Name,
|
||||||
|
Description = template.Description,
|
||||||
|
Author = template.Author,
|
||||||
|
Version = template.Version,
|
||||||
|
UpdateUrl = template.UpdateUrl,
|
||||||
|
DonateUrl = template.DonateUrl,
|
||||||
|
|
||||||
|
Files = new FilesConfigTransferModel
|
||||||
|
{
|
||||||
|
ConfigurationFiles = template.FilesConfig.ConfigurationFiles
|
||||||
|
.Select(cf => new ConfigurationFileTransferModel
|
||||||
|
{
|
||||||
|
Path = cf.Path,
|
||||||
|
Parser = cf.Parser,
|
||||||
|
Mappings = cf.Mappings
|
||||||
|
.Select(m => new ConfigurationFileMappingTransferModel { Key = m.Key, Value = m.Value })
|
||||||
|
.ToList()
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
},
|
||||||
|
|
||||||
|
Lifecycle = new LifecycleConfigTransferModel
|
||||||
|
{
|
||||||
|
StartupCommands = template.LifecycleConfig.StartupCommands
|
||||||
|
.Select(sc => new StartupCommandTransferModel
|
||||||
|
{ DisplayName = sc.DisplayName, Command = sc.Command })
|
||||||
|
.ToList(),
|
||||||
|
StopCommand = template.LifecycleConfig.StopCommand,
|
||||||
|
OnlineLogPatterns = template.LifecycleConfig.OnlineLogPatterns.ToList()
|
||||||
|
},
|
||||||
|
|
||||||
|
Installation = new InstallationConfigTransferModel
|
||||||
|
{
|
||||||
|
DockerImage = template.InstallationConfig.DockerImage,
|
||||||
|
Shell = template.InstallationConfig.Shell,
|
||||||
|
Script = template.InstallationConfig.Script
|
||||||
|
},
|
||||||
|
|
||||||
|
Miscellaneous = new MiscellaneousConfigTransferModel
|
||||||
|
{
|
||||||
|
UseLegacyStartup = template.MiscellaneousConfig.UseLegacyStartup
|
||||||
|
},
|
||||||
|
|
||||||
|
AllowUserDockerImageChange = template.AllowUserDockerImageChange,
|
||||||
|
DockerImages = template.DockerImages
|
||||||
|
.Select(img => new TemplateDockerImageTransferModel
|
||||||
|
{
|
||||||
|
DisplayName = img.DisplayName,
|
||||||
|
ImageName = img.ImageName,
|
||||||
|
SkipPulling = img.SkipPulling,
|
||||||
|
IsDefault = template.DefaultDockerImage != null && img.Id == template.DefaultDockerImage.Id
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
|
||||||
|
Variables = template.Variables
|
||||||
|
.Select(v => new TemplateVariableTransferModel
|
||||||
|
{
|
||||||
|
DisplayName = v.DisplayName,
|
||||||
|
Description = v.Description,
|
||||||
|
EnvName = v.EnvName,
|
||||||
|
DefaultValue = v.DefaultValue
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Template> ImportAsync(TemplateTransferModel m)
|
||||||
|
{
|
||||||
|
var template = new Template
|
||||||
|
{
|
||||||
|
Name = m.Name,
|
||||||
|
Description = m.Description,
|
||||||
|
Author = m.Author,
|
||||||
|
Version = m.Version,
|
||||||
|
UpdateUrl = m.UpdateUrl,
|
||||||
|
DonateUrl = m.DonateUrl,
|
||||||
|
|
||||||
|
FilesConfig = new FilesConfig
|
||||||
|
{
|
||||||
|
ConfigurationFiles = m.Files.ConfigurationFiles
|
||||||
|
.Select(cf => new ConfigurationFile
|
||||||
|
{
|
||||||
|
Path = cf.Path,
|
||||||
|
Parser = cf.Parser,
|
||||||
|
Mappings = cf.Mappings
|
||||||
|
.Select(mp => new ConfigurationFileMapping { Key = mp.Key, Value = mp.Value })
|
||||||
|
.ToList()
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
},
|
||||||
|
|
||||||
|
LifecycleConfig = new LifecycleConfig
|
||||||
|
{
|
||||||
|
StartupCommands = m.Lifecycle.StartupCommands
|
||||||
|
.Select(sc => new StartupCommand { DisplayName = sc.DisplayName, Command = sc.Command })
|
||||||
|
.ToList(),
|
||||||
|
StopCommand = m.Lifecycle.StopCommand,
|
||||||
|
OnlineLogPatterns = m.Lifecycle.OnlineLogPatterns.ToList()
|
||||||
|
},
|
||||||
|
|
||||||
|
InstallationConfig = new InstallationConfig
|
||||||
|
{
|
||||||
|
DockerImage = m.Installation.DockerImage,
|
||||||
|
Shell = m.Installation.Shell,
|
||||||
|
Script = m.Installation.Script
|
||||||
|
},
|
||||||
|
|
||||||
|
MiscellaneousConfig = new MiscellaneousConfig { UseLegacyStartup = m.Miscellaneous.UseLegacyStartup },
|
||||||
|
|
||||||
|
AllowUserDockerImageChange = m.AllowUserDockerImageChange,
|
||||||
|
|
||||||
|
Variables = m.Variables
|
||||||
|
.Select(v => new TemplateVariable
|
||||||
|
{
|
||||||
|
DisplayName = v.DisplayName,
|
||||||
|
Description = v.Description,
|
||||||
|
EnvName = v.EnvName,
|
||||||
|
DefaultValue = v.DefaultValue
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalTemplate = await TemplateRepository.AddAsync(template);
|
||||||
|
|
||||||
|
TemplateDockerImage? defaultDockerImage = null;
|
||||||
|
|
||||||
|
foreach (var img in m.DockerImages)
|
||||||
|
{
|
||||||
|
var entity = new TemplateDockerImage
|
||||||
|
{
|
||||||
|
DisplayName = img.DisplayName,
|
||||||
|
ImageName = img.ImageName,
|
||||||
|
SkipPulling = img.SkipPulling,
|
||||||
|
Template = template
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalEntity = await DockerImageRepository.AddAsync(entity);
|
||||||
|
|
||||||
|
if (img.IsDefault)
|
||||||
|
defaultDockerImage = finalEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalTemplate.DefaultDockerImage = defaultDockerImage;
|
||||||
|
|
||||||
|
await TemplateRepository.UpdateAsync(finalTemplate);
|
||||||
|
|
||||||
|
return finalTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
MoonlightServers.Api/Admin/Templates/TransferController.cs
Normal file
88
MoonlightServers.Api/Admin/Templates/TransferController.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using MoonlightServers.Shared.Admin.Templates;
|
||||||
|
using VYaml.Serialization;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/servers/templates")]
|
||||||
|
public class TransferController : Controller
|
||||||
|
{
|
||||||
|
private readonly TemplateTransferService TransferService;
|
||||||
|
|
||||||
|
public TransferController(TemplateTransferService transferService)
|
||||||
|
{
|
||||||
|
TransferService = transferService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:int}/export")]
|
||||||
|
public async Task<ActionResult> ExportAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var transferModel = await TransferService.ExportAsync(id);
|
||||||
|
|
||||||
|
if (transferModel == null)
|
||||||
|
return Problem("No template with that id found", statusCode: 404);
|
||||||
|
|
||||||
|
var yml = YamlSerializer.Serialize(transferModel, new YamlSerializerOptions
|
||||||
|
{
|
||||||
|
Resolver = CompositeResolver.Create([
|
||||||
|
GeneratedResolver.Instance,
|
||||||
|
StandardResolver.Instance
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
return File(yml.ToArray(), "text/yaml", $"{transferModel.Name}.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("import")]
|
||||||
|
public async Task<ActionResult<TemplateDto>> ImportAsync()
|
||||||
|
{
|
||||||
|
string content;
|
||||||
|
|
||||||
|
await using (Stream receiveStream = Request.Body)
|
||||||
|
|
||||||
|
using (StreamReader readStream = new StreamReader(receiveStream))
|
||||||
|
content = await readStream.ReadToEndAsync();
|
||||||
|
|
||||||
|
if(content.Contains("version: PLCN_v3"))
|
||||||
|
{
|
||||||
|
var importService = HttpContext.RequestServices.GetRequiredService<PelicanEggImportService>();
|
||||||
|
|
||||||
|
var template = await importService.ImportAsync(content);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(template);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
content.Contains("PTDL_v2", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
content.Contains("PLCN_v1", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
content.Contains("PLCN_v2", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
content.Contains("PLCN_v3", StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var importService = HttpContext.RequestServices.GetRequiredService<PterodactylEggImportService>();
|
||||||
|
|
||||||
|
var template = await importService.ImportAsync(content);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(template);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var transferModel = YamlSerializer.Deserialize<TemplateTransferModel>(
|
||||||
|
Encoding.UTF8.GetBytes(content),
|
||||||
|
new YamlSerializerOptions
|
||||||
|
{
|
||||||
|
Resolver = CompositeResolver.Create([
|
||||||
|
GeneratedResolver.Instance,
|
||||||
|
StandardResolver.Instance
|
||||||
|
])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var template = await TransferService.ImportAsync(transferModel);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
MoonlightServers.Api/Admin/Templates/VariablesController.cs
Normal file
144
MoonlightServers.Api/Admin/Templates/VariablesController.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Shared.Http.Responses;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
using MoonlightServers.Shared;
|
||||||
|
using MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Admin.Templates;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/servers/templates/{templateId:int}/variables")]
|
||||||
|
public class VariablesController : Controller
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<TemplateVariable> VariableRepository;
|
||||||
|
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||||
|
|
||||||
|
public VariablesController(
|
||||||
|
DatabaseRepository<TemplateVariable> variableRepository,
|
||||||
|
DatabaseRepository<Template> templateRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
VariableRepository = variableRepository;
|
||||||
|
TemplateRepository = templateRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize(Policy = Permissions.Templates.View)]
|
||||||
|
public async Task<ActionResult<PagedData<VariableDto>>> GetAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromQuery] int startIndex,
|
||||||
|
[FromQuery] int length
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Validation
|
||||||
|
if (startIndex < 0)
|
||||||
|
return Problem("Invalid start index specified", statusCode: 400);
|
||||||
|
|
||||||
|
if (length is < 1 or > 100)
|
||||||
|
return Problem("Invalid length specified");
|
||||||
|
|
||||||
|
if (!await TemplateRepository.Query().AnyAsync(x => x.Id == templateId))
|
||||||
|
return Problem("No template with that id found", statusCode: 404);
|
||||||
|
|
||||||
|
// Query building
|
||||||
|
|
||||||
|
var query = VariableRepository
|
||||||
|
.Query()
|
||||||
|
.Where(x => x.Template.Id == templateId);
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
var data = await query
|
||||||
|
.OrderBy(x => x.Id)
|
||||||
|
.ProjectToDto()
|
||||||
|
.Skip(startIndex)
|
||||||
|
.Take(length)
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
|
||||||
|
return new PagedData<VariableDto>(data, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.View)]
|
||||||
|
public async Task<ActionResult<VariableDto>> GetAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromRoute] int id
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var templateVariable = await VariableRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||||
|
|
||||||
|
if (templateVariable == null)
|
||||||
|
return Problem("No template or template variable found with that id");
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(templateVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Create)]
|
||||||
|
public async Task<ActionResult<VariableDto>> CreateAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromBody] CreateVariableDto dto
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var template = await TemplateRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == templateId);
|
||||||
|
|
||||||
|
if (template == null)
|
||||||
|
return Problem("No template with that id found", statusCode: 404);
|
||||||
|
|
||||||
|
var variable = TemplateMapper.ToEntity(dto);
|
||||||
|
|
||||||
|
variable.Template = template;
|
||||||
|
|
||||||
|
var finalVariable = await VariableRepository.AddAsync(variable);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(finalVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Edit)]
|
||||||
|
public async Task<ActionResult<VariableDto>> UpdateAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromRoute] int id,
|
||||||
|
[FromBody] UpdateVariableDto dto
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var templateVariable = await VariableRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||||
|
|
||||||
|
if (templateVariable == null)
|
||||||
|
return Problem("No template or template variable found with that id");
|
||||||
|
|
||||||
|
TemplateMapper.Merge(templateVariable, dto);
|
||||||
|
|
||||||
|
await VariableRepository.UpdateAsync(templateVariable);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(templateVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
[Authorize(Policy = Permissions.Templates.Delete)]
|
||||||
|
public async Task<ActionResult> DeleteAsync(
|
||||||
|
[FromRoute] int templateId,
|
||||||
|
[FromRoute] int id
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var templateVariable = await VariableRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||||
|
|
||||||
|
if (templateVariable == null)
|
||||||
|
return Problem("No template or template variable found with that id");
|
||||||
|
|
||||||
|
await VariableRepository.RemoveAsync(templateVariable);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using MoonlightServers.Shared.Http.Requests;
|
|
||||||
using MoonlightServers.Shared.Http.Responses;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Api.Http.Controllers;
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/form")]
|
|
||||||
public class FormController : Controller
|
|
||||||
{
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<ActionResult<FormResultDto>> PostAsync([FromBody] FormSubmitDto dto)
|
|
||||||
{
|
|
||||||
return new FormResultDto()
|
|
||||||
{
|
|
||||||
Result = dto.TextField.Replace(" ", string.Empty)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MoonlightServers.Api.Infrastructure.Configuration;
|
||||||
|
|
||||||
|
public class NodeTokenOptions
|
||||||
|
{
|
||||||
|
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||||
|
}
|
||||||
61
MoonlightServers.Api/Infrastructure/Database/DataContext.cs
Normal file
61
MoonlightServers.Api/Infrastructure/Database/DataContext.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moonlight.Api.Configuration;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
|
||||||
|
public class DataContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<Node> Nodes { get; set; }
|
||||||
|
public DbSet<Template> Templates { get; set; }
|
||||||
|
public DbSet<TemplateDockerImage> TemplateDockerImages { get; set; }
|
||||||
|
public DbSet<TemplateVariable> TemplateVariablesVariables { get; set; }
|
||||||
|
|
||||||
|
private readonly IOptions<DatabaseOptions> Options;
|
||||||
|
public DataContext(IOptions<DatabaseOptions> options)
|
||||||
|
{
|
||||||
|
Options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
if (optionsBuilder.IsConfigured)
|
||||||
|
return;
|
||||||
|
|
||||||
|
optionsBuilder.UseNpgsql(
|
||||||
|
$"Host={Options.Value.Host};" +
|
||||||
|
$"Port={Options.Value.Port};" +
|
||||||
|
$"Username={Options.Value.Username};" +
|
||||||
|
$"Password={Options.Value.Password};" +
|
||||||
|
$"Database={Options.Value.Database}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.HasDefaultSchema("servers");
|
||||||
|
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Template>()
|
||||||
|
.ComplexProperty(x => x.FilesConfig, builder => builder.ToJson())
|
||||||
|
.ComplexProperty(x => x.LifecycleConfig, builder => builder.ToJson())
|
||||||
|
.ComplexProperty(x => x.InstallationConfig, builder => builder.ToJson())
|
||||||
|
.ComplexProperty(x => x.MiscellaneousConfig, builder => builder.ToJson());
|
||||||
|
|
||||||
|
// One-to-many: Template => DockerImages
|
||||||
|
modelBuilder.Entity<Template>()
|
||||||
|
.HasMany(t => t.DockerImages)
|
||||||
|
.WithOne(d => d.Template)
|
||||||
|
.HasForeignKey("TemplateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// One-to-one: Template => DefaultDockerImage
|
||||||
|
modelBuilder.Entity<Template>()
|
||||||
|
.HasOne(t => t.DefaultDockerImage)
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey<Template>("DefaultDockerImageId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Interfaces;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
|
||||||
|
public class DatabaseRepository<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly DataContext DataContext;
|
||||||
|
private readonly DbSet<T> Set;
|
||||||
|
|
||||||
|
public DatabaseRepository(DataContext dataContext)
|
||||||
|
{
|
||||||
|
DataContext = dataContext;
|
||||||
|
Set = DataContext.Set<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryable<T> Query() => Set;
|
||||||
|
|
||||||
|
public async Task<T> AddAsync(T entity)
|
||||||
|
{
|
||||||
|
if (entity is IActionTimestamps actionTimestamps)
|
||||||
|
{
|
||||||
|
actionTimestamps.CreatedAt = DateTimeOffset.UtcNow;
|
||||||
|
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
var final = Set.Add(entity);
|
||||||
|
await DataContext.SaveChangesAsync();
|
||||||
|
return final.Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(T entity)
|
||||||
|
{
|
||||||
|
if (entity is IActionTimestamps actionTimestamps)
|
||||||
|
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
Set.Update(entity);
|
||||||
|
await DataContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(T entity)
|
||||||
|
{
|
||||||
|
Set.Remove(entity);
|
||||||
|
await DataContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
|
||||||
|
public class DbMigrationService : IHostedLifecycleService
|
||||||
|
{
|
||||||
|
private readonly ILogger<DbMigrationService> Logger;
|
||||||
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
|
||||||
|
public DbMigrationService(ILogger<DbMigrationService> logger, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartingAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Checking for pending migrations");
|
||||||
|
|
||||||
|
await using var scope = ServiceProvider.CreateAsyncScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||||
|
|
||||||
|
var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken);
|
||||||
|
var migrationNames = pendingMigrations.ToArray();
|
||||||
|
|
||||||
|
if (migrationNames.Length == 0)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("No pending migrations found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInformation("Pending migrations: {names}", string.Join(", ", migrationNames));
|
||||||
|
Logger.LogInformation("Migration started");
|
||||||
|
|
||||||
|
await context.Database.MigrateAsync(cancellationToken);
|
||||||
|
|
||||||
|
Logger.LogInformation("Migration complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Interfaces;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
|
||||||
|
public class Node : IActionTimestamps
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string HttpEndpointUrl { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(10)]
|
||||||
|
public string TokenId { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
// Action timestamps
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
|
||||||
|
public class Template
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Author { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(2048)]
|
||||||
|
public string? UpdateUrl { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(2048)]
|
||||||
|
public string? DonateUrl { get; set; }
|
||||||
|
|
||||||
|
// JSON Options
|
||||||
|
public FilesConfig FilesConfig { get; set; }
|
||||||
|
public LifecycleConfig LifecycleConfig { get; set; }
|
||||||
|
public InstallationConfig InstallationConfig { get; set; }
|
||||||
|
public MiscellaneousConfig MiscellaneousConfig { get; set; }
|
||||||
|
|
||||||
|
// Docker Images
|
||||||
|
public bool AllowUserDockerImageChange { get; set; }
|
||||||
|
public TemplateDockerImage? DefaultDockerImage { get; set; }
|
||||||
|
public List<TemplateDockerImage> DockerImages { get; set; } = new();
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
public List<TemplateVariable> Variables { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
|
||||||
|
public class TemplateDockerImage
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string ImageName { get; set; }
|
||||||
|
|
||||||
|
public bool SkipPulling { get; set; }
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
public Template Template { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
|
||||||
|
public class TemplateVariable
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(60)]
|
||||||
|
public string EnvName { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(1024)]
|
||||||
|
public string? DefaultValue { get; set; }
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
public Template Template { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Interfaces;
|
||||||
|
|
||||||
|
internal interface IActionTimestamps
|
||||||
|
{
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
|
||||||
|
public class FilesConfig
|
||||||
|
{
|
||||||
|
public List<ConfigurationFile> ConfigurationFiles { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigurationFile
|
||||||
|
{
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
public string Parser { get; set; } = string.Empty;
|
||||||
|
public List<ConfigurationFileMapping> Mappings { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigurationFileMapping
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
public string? Value { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
|
||||||
|
public class InstallationConfig
|
||||||
|
{
|
||||||
|
public string DockerImage { get; set; } = string.Empty;
|
||||||
|
public string Shell { get; set; } = string.Empty;
|
||||||
|
public string Script { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
|
||||||
|
public class LifecycleConfig
|
||||||
|
{
|
||||||
|
public List<StartupCommand> StartupCommands { get; set; } = [];
|
||||||
|
public string StopCommand { get; set; } = string.Empty;
|
||||||
|
public List<string> OnlineLogPatterns { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StartupCommand
|
||||||
|
{
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public string Command { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Json;
|
||||||
|
|
||||||
|
public class MiscellaneousConfig
|
||||||
|
{
|
||||||
|
public bool UseLegacyStartup { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20260305104238_AddedBasicNodeEntity")]
|
||||||
|
partial class AddedBasicNodeEntity
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("servers")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("HttpEndpointUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Nodes", "servers");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedBasicNodeEntity : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.EnsureSchema(
|
||||||
|
name: "servers");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Nodes",
|
||||||
|
schema: "servers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
HttpEndpointUrl = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
TokenId = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||||
|
Token = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Nodes", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Nodes",
|
||||||
|
schema: "servers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
315
MoonlightServers.Api/Infrastructure/Database/Migrations/20260312075719_AddedTemplateEntities.Designer.cs
generated
Normal file
315
MoonlightServers.Api/Infrastructure/Database/Migrations/20260312075719_AddedTemplateEntities.Designer.cs
generated
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20260312075719_AddedTemplateEntities")]
|
||||||
|
partial class AddedTemplateEntities
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("servers")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("HttpEndpointUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Nodes", "servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("AllowUserDockerImageChange")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Author")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<int?>("DefaultDockerImageId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("DonateUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("UpdateUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
|
||||||
|
{
|
||||||
|
b1.IsRequired();
|
||||||
|
|
||||||
|
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
|
||||||
|
{
|
||||||
|
b2.IsRequired();
|
||||||
|
|
||||||
|
b2.Property<string>("Parser")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b2.Property<string>("Path")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
|
||||||
|
{
|
||||||
|
b3.IsRequired();
|
||||||
|
|
||||||
|
b3.Property<string>("Key")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b3.Property<string>("Value")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("FilesConfig")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
|
||||||
|
{
|
||||||
|
b1.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<string>("DockerImage")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<string>("Script")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<string>("Shell")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("InstallationConfig")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
|
||||||
|
{
|
||||||
|
b1.IsRequired();
|
||||||
|
|
||||||
|
b1.PrimitiveCollection<string>("OnlineLogPatterns")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<string>("StopCommand")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
|
||||||
|
{
|
||||||
|
b2.IsRequired();
|
||||||
|
|
||||||
|
b2.Property<string>("Command")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b2.Property<string>("DisplayName")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("LifecycleConfig")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
|
||||||
|
{
|
||||||
|
b1.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<bool>("UseLegacyStartup");
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("MiscellaneousConfig")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DefaultDockerImageId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Templates", "servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("ImageName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("SkipPulling")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("TemplateId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TemplateId");
|
||||||
|
|
||||||
|
b.ToTable("TemplateDockerImages", "servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DefaultValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("EnvName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(60)
|
||||||
|
.HasColumnType("character varying(60)");
|
||||||
|
|
||||||
|
b.Property<int>("TemplateId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TemplateId");
|
||||||
|
|
||||||
|
b.ToTable("TemplateVariablesVariables", "servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("DefaultDockerImage");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||||
|
.WithMany("DockerImages")
|
||||||
|
.HasForeignKey("TemplateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Template");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||||
|
.WithMany("Variables")
|
||||||
|
.HasForeignKey("TemplateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Template");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DockerImages");
|
||||||
|
|
||||||
|
b.Navigation("Variables");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedTemplateEntities : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TemplateDockerImages",
|
||||||
|
schema: "servers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
DisplayName = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
ImageName = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||||
|
SkipPulling = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
TemplateId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TemplateDockerImages", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Templates",
|
||||||
|
schema: "servers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||||
|
Author = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
Version = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
UpdateUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
DonateUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
AllowUserDockerImageChange = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
DefaultDockerImageId = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
FilesConfig = table.Column<string>(type: "jsonb", nullable: false),
|
||||||
|
InstallationConfig = table.Column<string>(type: "jsonb", nullable: false),
|
||||||
|
LifecycleConfig = table.Column<string>(type: "jsonb", nullable: false),
|
||||||
|
MiscellaneousConfig = table.Column<string>(type: "jsonb", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Templates", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Templates_TemplateDockerImages_DefaultDockerImageId",
|
||||||
|
column: x => x.DefaultDockerImageId,
|
||||||
|
principalSchema: "servers",
|
||||||
|
principalTable: "TemplateDockerImages",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TemplateVariablesVariables",
|
||||||
|
schema: "servers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
DisplayName = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||||
|
EnvName = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
|
||||||
|
DefaultValue = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||||
|
TemplateId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TemplateVariablesVariables", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TemplateVariablesVariables_Templates_TemplateId",
|
||||||
|
column: x => x.TemplateId,
|
||||||
|
principalSchema: "servers",
|
||||||
|
principalTable: "Templates",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TemplateDockerImages_TemplateId",
|
||||||
|
schema: "servers",
|
||||||
|
table: "TemplateDockerImages",
|
||||||
|
column: "TemplateId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Templates_DefaultDockerImageId",
|
||||||
|
schema: "servers",
|
||||||
|
table: "Templates",
|
||||||
|
column: "DefaultDockerImageId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TemplateVariablesVariables_TemplateId",
|
||||||
|
schema: "servers",
|
||||||
|
table: "TemplateVariablesVariables",
|
||||||
|
column: "TemplateId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_TemplateDockerImages_Templates_TemplateId",
|
||||||
|
schema: "servers",
|
||||||
|
table: "TemplateDockerImages",
|
||||||
|
column: "TemplateId",
|
||||||
|
principalSchema: "servers",
|
||||||
|
principalTable: "Templates",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_TemplateDockerImages_Templates_TemplateId",
|
||||||
|
schema: "servers",
|
||||||
|
table: "TemplateDockerImages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TemplateVariablesVariables",
|
||||||
|
schema: "servers");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Templates",
|
||||||
|
schema: "servers");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TemplateDockerImages",
|
||||||
|
schema: "servers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
partial class DataContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("servers")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("HttpEndpointUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Nodes", "servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("AllowUserDockerImageChange")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Author")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<int?>("DefaultDockerImageId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("DonateUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("UpdateUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
|
||||||
|
{
|
||||||
|
b1.IsRequired();
|
||||||
|
|
||||||
|
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
|
||||||
|
{
|
||||||
|
b2.IsRequired();
|
||||||
|
|
||||||
|
b2.Property<string>("Parser")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b2.Property<string>("Path")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
|
||||||
|
{
|
||||||
|
b3.IsRequired();
|
||||||
|
|
||||||
|
b3.Property<string>("Key")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b3.Property<string>("Value")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("FilesConfig")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
|
||||||
|
{
|
||||||
|
b1.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<string>("DockerImage")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<string>("Script")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<string>("Shell")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("InstallationConfig")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
|
||||||
|
{
|
||||||
|
b1.IsRequired();
|
||||||
|
|
||||||
|
b1.PrimitiveCollection<string>("OnlineLogPatterns")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<string>("StopCommand")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
|
||||||
|
{
|
||||||
|
b2.IsRequired();
|
||||||
|
|
||||||
|
b2.Property<string>("Command")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b2.Property<string>("DisplayName")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("LifecycleConfig")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
|
||||||
|
{
|
||||||
|
b1.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<bool>("UseLegacyStartup");
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("MiscellaneousConfig")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DefaultDockerImageId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Templates", "servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("ImageName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("SkipPulling")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("TemplateId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TemplateId");
|
||||||
|
|
||||||
|
b.ToTable("TemplateDockerImages", "servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DefaultValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("EnvName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(60)
|
||||||
|
.HasColumnType("character varying(60)");
|
||||||
|
|
||||||
|
b.Property<int>("TemplateId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TemplateId");
|
||||||
|
|
||||||
|
b.ToTable("TemplateVariablesVariables", "servers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("DefaultDockerImage");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||||
|
.WithMany("DockerImages")
|
||||||
|
.HasForeignKey("TemplateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Template");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||||
|
.WithMany("Variables")
|
||||||
|
.HasForeignKey("TemplateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Template");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DockerImages");
|
||||||
|
|
||||||
|
b.Navigation("Variables");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||||
|
|
||||||
|
public class NodeTokenSchemeHandler : AuthenticationHandler<NodeTokenSchemeOptions>
|
||||||
|
{
|
||||||
|
public const string SchemeName = "MoonlightServers.NodeToken";
|
||||||
|
public const string CacheKeyFormat = $"MoonlightServers.{nameof(NodeTokenSchemeHandler)}.{{0}}";
|
||||||
|
|
||||||
|
private readonly DatabaseRepository<Node> DatabaseRepository;
|
||||||
|
private readonly HybridCache Cache;
|
||||||
|
|
||||||
|
public NodeTokenSchemeHandler(
|
||||||
|
IOptionsMonitor<NodeTokenSchemeOptions> options,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder,
|
||||||
|
DatabaseRepository<Node> databaseRepository,
|
||||||
|
HybridCache cache
|
||||||
|
) : base(options, logger, encoder)
|
||||||
|
{
|
||||||
|
DatabaseRepository = databaseRepository;
|
||||||
|
Cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
// Basic format validation
|
||||||
|
|
||||||
|
if (!Context.Request.Headers.TryGetValue(HeaderNames.Authorization, out var authHeaderValues))
|
||||||
|
return AuthenticateResult.Fail("No authorization header present");
|
||||||
|
|
||||||
|
if (authHeaderValues.Count != 1)
|
||||||
|
return AuthenticateResult.Fail("No authorization value present");
|
||||||
|
|
||||||
|
var authHeaderValue = authHeaderValues[0];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(authHeaderValue))
|
||||||
|
return AuthenticateResult.Fail("No authorization value present");
|
||||||
|
|
||||||
|
var authHeaderParts = authHeaderValue.Split(
|
||||||
|
' ',
|
||||||
|
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate parts
|
||||||
|
if (authHeaderParts.Length < 2)
|
||||||
|
return AuthenticateResult.Fail("Malformed authorization header");
|
||||||
|
|
||||||
|
var tokenId = authHeaderParts[0];
|
||||||
|
var token = authHeaderParts[1];
|
||||||
|
|
||||||
|
if (tokenId.Length != 10 && token.Length != 64)
|
||||||
|
return AuthenticateResult.Fail("Malformed authorization header");
|
||||||
|
|
||||||
|
// Real validation
|
||||||
|
|
||||||
|
var cacheKey = string.Format(CacheKeyFormat, tokenId);
|
||||||
|
|
||||||
|
var session = await Cache.GetOrCreateAsync<NodeTokenSession?>(cacheKey, async cancellationToken =>
|
||||||
|
{
|
||||||
|
return await DatabaseRepository
|
||||||
|
.Query()
|
||||||
|
.Where(x => x.TokenId == tokenId)
|
||||||
|
.Select(x => new NodeTokenSession(x.Id, x.Token))
|
||||||
|
.FirstOrDefaultAsync(cancellationToken: cancellationToken);
|
||||||
|
},
|
||||||
|
new HybridCacheEntryOptions()
|
||||||
|
{
|
||||||
|
LocalCacheExpiration = Options.LookupCacheL1Expiry,
|
||||||
|
Expiration = Options.LookupCacheL2Expiry
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if(session == null || token != session.Token)
|
||||||
|
return AuthenticateResult.Fail("Invalid authorization header");
|
||||||
|
|
||||||
|
// All checks have passed, create auth ticket
|
||||||
|
return AuthenticateResult.Success(new AuthenticationTicket(
|
||||||
|
new ClaimsPrincipal(
|
||||||
|
new ClaimsIdentity(
|
||||||
|
[
|
||||||
|
new Claim("NodeId", session.Id.ToString())
|
||||||
|
],
|
||||||
|
SchemeName
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SchemeName
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record NodeTokenSession(int Id, string Token);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||||
|
|
||||||
|
public class NodeTokenSchemeOptions : AuthenticationSchemeOptions
|
||||||
|
{
|
||||||
|
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||||
|
}
|
||||||
@@ -12,24 +12,19 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
|
||||||
<PackageReference Include="Moonlight.Api" Version="2.1.0">
|
<PackageReference Include="Moonlight.Api" Version="2.1.0">
|
||||||
<ExcludeAssets>content;contentfiles</ExcludeAssets>
|
<ExcludeAssets>content;contentfiles</ExcludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Configuration\"/>
|
<Folder Include="Client\" />
|
||||||
<Folder Include="Database\Entities\"/>
|
|
||||||
<Folder Include="Database\Migrations\"/>
|
|
||||||
<Folder Include="Helpers\"/>
|
|
||||||
<Folder Include="Implementations\"/>
|
|
||||||
<Folder Include="Interfaces\"/>
|
|
||||||
<Folder Include="Mappers\"/>
|
|
||||||
<Folder Include="Models\"/>
|
|
||||||
<Folder Include="Services\"/>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
|
||||||
<ProjectReference Include="..\MoonlightServers.Shared\MoonlightServers.Shared.csproj" />
|
<ProjectReference Include="..\MoonlightServers.Shared\MoonlightServers.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
14
MoonlightServers.Api/Remote/Nodes/PingController.cs
Normal file
14
MoonlightServers.Api/Remote/Nodes/PingController.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Api.Remote.Nodes;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/remote/servers/nodes/ping")]
|
||||||
|
[Authorize(AuthenticationSchemes = NodeTokenSchemeHandler.SchemeName)]
|
||||||
|
public class PingController : Controller
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public ActionResult Get() => NoContent();
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Moonlight.Api;
|
using Moonlight.Api;
|
||||||
|
using MoonlightServers.Api.Admin.Nodes;
|
||||||
|
using MoonlightServers.Api.Admin.Templates;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Configuration;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Database;
|
||||||
|
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||||
using SimplePlugin.Abstractions;
|
using SimplePlugin.Abstractions;
|
||||||
using SerializationContext = MoonlightServers.Shared.SerializationContext;
|
using SerializationContext = MoonlightServers.Shared.SerializationContext;
|
||||||
|
|
||||||
@@ -17,5 +24,32 @@ public class Startup : MoonlightPlugin
|
|||||||
{
|
{
|
||||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped(typeof(DatabaseRepository<>));
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<DataContext>();
|
||||||
|
builder.Services.AddHostedService<DbMigrationService>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<TemplateTransferService>();
|
||||||
|
builder.Services.AddScoped<PterodactylEggImportService>();
|
||||||
|
builder.Services.AddScoped<PelicanEggImportService>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<NodeService>();
|
||||||
|
|
||||||
|
var nodeTokenOptions = new NodeTokenOptions();
|
||||||
|
builder.Configuration.Bind("Moonlight:Servers:NodeToken", nodeTokenOptions);
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddAuthentication()
|
||||||
|
.AddScheme<NodeTokenSchemeOptions, NodeTokenSchemeHandler>(NodeTokenSchemeHandler.SchemeName, options =>
|
||||||
|
{
|
||||||
|
options.LookupCacheL1Expiry = nodeTokenOptions.LookupCacheL1Expiry;
|
||||||
|
options.LookupCacheL2Expiry = nodeTokenOptions.LookupCacheL2Expiry;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Logging.AddFilter(
|
||||||
|
"MoonlightServers.Api.Infrastructure.Implementations.NodeToken.NodeTokenSchemeHandler",
|
||||||
|
LogLevel.Warning
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
MoonlightServers.Daemon/Configuration/RemoteOptions.cs
Normal file
8
MoonlightServers.Daemon/Configuration/RemoteOptions.cs
Normal 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; }
|
||||||
|
}
|
||||||
30
MoonlightServers.Daemon/Http/Controllers/HealthController.cs
Normal file
30
MoonlightServers.Daemon/Http/Controllers/HealthController.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Implementations.TokenScheme;
|
||||||
|
|
||||||
|
public class TokenSchemeOptions : AuthenticationSchemeOptions
|
||||||
|
{
|
||||||
|
public string Token { get; set; }
|
||||||
|
}
|
||||||
@@ -28,4 +28,8 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using Microsoft.Extensions.Logging.Console;
|
using Microsoft.Extensions.Logging.Console;
|
||||||
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
using MoonlightServers.Daemon.Helpers;
|
||||||
|
using MoonlightServers.Daemon.Implementations.TokenScheme;
|
||||||
using MoonlightServers.Daemon.ServerSystem;
|
using MoonlightServers.Daemon.ServerSystem;
|
||||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Local;
|
using MoonlightServers.Daemon.ServerSystem.Implementations.Local;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
using MoonlightServers.DaemonShared.Http;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -18,7 +21,28 @@ builder.Services.AddSingleton<ServerService>();
|
|||||||
builder.Services.AddDockerServices();
|
builder.Services.AddDockerServices();
|
||||||
builder.Services.AddLocalServices();
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -50,8 +74,6 @@ Task.Run(async () =>
|
|||||||
await server.StopAsync();
|
await server.StopAsync();
|
||||||
|
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
|
|
||||||
await serverService.DeleteAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
117
MoonlightServers.Daemon/Services/RemoteService.cs
Normal file
117
MoonlightServers.Daemon/Services/RemoteService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
MoonlightServers.DaemonShared/Http/Daemon/HealthDto.cs
Normal file
6
MoonlightServers.DaemonShared/Http/Daemon/HealthDto.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MoonlightServers.DaemonShared.Http.Daemon;
|
||||||
|
|
||||||
|
public class HealthDto
|
||||||
|
{
|
||||||
|
public int RemoteStatusCode { get; set; }
|
||||||
|
}
|
||||||
14
MoonlightServers.DaemonShared/Http/ProblemDetails.cs
Normal file
14
MoonlightServers.DaemonShared/Http/ProblemDetails.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace MoonlightServers.DaemonShared.Http;
|
||||||
|
|
||||||
|
public class ProblemDetails
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
public int Status { get; set; }
|
||||||
|
|
||||||
|
public string? Detail { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, string[]>? Errors { get; set; }
|
||||||
|
}
|
||||||
14
MoonlightServers.DaemonShared/Http/SerializationContext.cs
Normal file
14
MoonlightServers.DaemonShared/Http/SerializationContext.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||||
|
|
||||||
|
namespace MoonlightServers.DaemonShared.Http;
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(HealthDto))]
|
||||||
|
[JsonSerializable(typeof(ProblemDetails))]
|
||||||
|
|
||||||
|
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||||
|
public partial class SerializationContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,4 +6,8 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Http\Panel\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
75
MoonlightServers.Frontend/Admin/Index.razor
Normal file
75
MoonlightServers.Frontend/Admin/Index.razor
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
@page "/admin/servers"
|
||||||
|
|
||||||
|
@using LucideBlazor
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using ShadcnBlazor.Tab
|
||||||
|
|
||||||
|
@inject IAuthorizationService AuthorizationService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
@attribute [Authorize(Policy = Permissions.Servers.View)]
|
||||||
|
|
||||||
|
<Tabs DefaultValue="@(Tab ?? "servers")" OnValueChanged="OnTabChanged">
|
||||||
|
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
|
||||||
|
<TabsTrigger Value="servers" Disabled="@(!ServersAccess.Succeeded)">
|
||||||
|
<ContainerIcon />
|
||||||
|
Servers
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger Value="nodes" Disabled="@(!NodesAccess.Succeeded)">
|
||||||
|
<ServerIcon />
|
||||||
|
Nodes
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger Value="templates" Disabled="@(!TemplatesAccess.Succeeded)">
|
||||||
|
<Package2Icon />
|
||||||
|
Templates
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger Value="manager" Disabled="@(!NodesAccess.Succeeded)">
|
||||||
|
<TableIcon />
|
||||||
|
Manager
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
@if (ServersAccess.Succeeded)
|
||||||
|
{
|
||||||
|
<TabsContent Value="servers">
|
||||||
|
|
||||||
|
</TabsContent>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (NodesAccess.Succeeded)
|
||||||
|
{
|
||||||
|
<TabsContent Value="nodes">
|
||||||
|
<MoonlightServers.Frontend.Admin.Nodes.Overview />
|
||||||
|
</TabsContent>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (TemplatesAccess.Succeeded)
|
||||||
|
{
|
||||||
|
<TabsContent Value="templates">
|
||||||
|
<MoonlightServers.Frontend.Admin.Templates.Overview />
|
||||||
|
</TabsContent>
|
||||||
|
}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
[SupplyParameterFromQuery(Name = "tab")]
|
||||||
|
public string? Tab { get; set; }
|
||||||
|
|
||||||
|
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||||
|
|
||||||
|
private AuthorizationResult ServersAccess;
|
||||||
|
private AuthorizationResult NodesAccess;
|
||||||
|
private AuthorizationResult TemplatesAccess;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var authState = await AuthState;
|
||||||
|
|
||||||
|
ServersAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Servers.View);
|
||||||
|
NodesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.View);
|
||||||
|
TemplatesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.View);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTabChanged(string tab) => Navigation.NavigateTo($"/admin/servers?tab={tab}");
|
||||||
|
}
|
||||||
104
MoonlightServers.Frontend/Admin/Nodes/Create.razor
Normal file
104
MoonlightServers.Frontend/Admin/Nodes/Create.razor
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
@page "/admin/servers/nodes/create"
|
||||||
|
|
||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Nodes
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Cards
|
||||||
|
@using ShadcnBlazor.Extras.Forms
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-xl font-semibold">Create Node</h1>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Create a new node
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-x-1.5">
|
||||||
|
<Button Variant="ButtonVariant.Secondary">
|
||||||
|
<Slot>
|
||||||
|
<a href="/admin/servers?tab=nodes" @attributes="context">
|
||||||
|
<ChevronLeftIcon/>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
</Slot>
|
||||||
|
</Button>
|
||||||
|
<SubmitButton>
|
||||||
|
<CheckIcon/>
|
||||||
|
Continue
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
|
<FormValidationSummary/>
|
||||||
|
|
||||||
|
<FieldSet>
|
||||||
|
<FieldGroup>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="nodeName">Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.Name"
|
||||||
|
id="nodeName"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="nodeHttpEndpoint">HTTP Endpoint</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.HttpEndpointUrl"
|
||||||
|
id="nodeHttpEndpoint"
|
||||||
|
placeholder="http://example.com:8080"/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</FieldGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</EnhancedEditForm>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private CreateNodeDto Request = new();
|
||||||
|
|
||||||
|
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.PostAsJsonAsync(
|
||||||
|
"/api/admin/servers/nodes",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Node Creation",
|
||||||
|
$"Successfully created node {Request.Name}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Navigation.NavigateTo("/admin/servers?tab=nodes");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
154
MoonlightServers.Frontend/Admin/Nodes/Edit.razor
Normal file
154
MoonlightServers.Frontend/Admin/Nodes/Edit.razor
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
@page "/admin/servers/nodes/{Id:int}"
|
||||||
|
|
||||||
|
@using System.Net
|
||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Nodes
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Cards
|
||||||
|
@using ShadcnBlazor.Emptys
|
||||||
|
@using ShadcnBlazor.Extras.Common
|
||||||
|
@using ShadcnBlazor.Extras.Forms
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<LazyLoader Load="LoadAsync">
|
||||||
|
@if (Node == null)
|
||||||
|
{
|
||||||
|
<Empty>
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||||
|
<SearchIcon/>
|
||||||
|
</EmptyMedia>
|
||||||
|
<EmptyTitle>Node not found</EmptyTitle>
|
||||||
|
<EmptyDescription>
|
||||||
|
A node with this id cannot be found
|
||||||
|
</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
</Empty>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-xl font-semibold">Update Node</h1>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Update node @Node.Name
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-x-1.5">
|
||||||
|
<Button Variant="ButtonVariant.Secondary">
|
||||||
|
<Slot>
|
||||||
|
<a href="/admin/servers?tab=nodes" @attributes="context">
|
||||||
|
<ChevronLeftIcon/>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
</Slot>
|
||||||
|
</Button>
|
||||||
|
<SubmitButton>
|
||||||
|
<CheckIcon/>
|
||||||
|
Continue
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
|
<FormValidationSummary/>
|
||||||
|
|
||||||
|
<FieldSet>
|
||||||
|
<FieldGroup>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="nodeName">Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.Name"
|
||||||
|
id="nodeName"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="nodeHttpEndpoint">HTTP Endpoint</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.HttpEndpointUrl"
|
||||||
|
id="nodeHttpEndpoint"
|
||||||
|
placeholder="http://example.com:8080"/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</FieldGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</EnhancedEditForm>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public int Id { get; set; }
|
||||||
|
|
||||||
|
private UpdateNodeDto Request;
|
||||||
|
private NodeDto? Node;
|
||||||
|
|
||||||
|
private async Task LoadAsync(LazyLoader _)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.GetAsync($"api/admin/servers/nodes/{Id}");
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
if(response.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
return;
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node = await response.Content.ReadFromJsonAsync<NodeDto>(SerializationContext.Default.Options);
|
||||||
|
|
||||||
|
if(Node == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Request = new UpdateNodeDto()
|
||||||
|
{
|
||||||
|
Name = Node.Name,
|
||||||
|
HttpEndpointUrl = Node.HttpEndpointUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.PutAsJsonAsync(
|
||||||
|
$"/api/admin/servers/nodes/{Id}",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Node Update",
|
||||||
|
$"Successfully updated node {Request.Name}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Navigation.NavigateTo("/admin/servers?tab=nodes");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
MoonlightServers.Frontend/Admin/Nodes/NodeHealthDisplay.razor
Normal file
109
MoonlightServers.Frontend/Admin/Nodes/NodeHealthDisplay.razor
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
@using Microsoft.Extensions.Logging
|
||||||
|
@using MoonlightServers.Shared.Admin.Nodes
|
||||||
|
@using ShadcnBlazor.Tooltips
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ILogger<NodeHealthDisplay> Logger
|
||||||
|
|
||||||
|
@if (IsLoading)
|
||||||
|
{
|
||||||
|
<span class="text-muted-foreground">Loading</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IsHealthy)
|
||||||
|
{
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Slot>
|
||||||
|
<span class="text-green-400" @attributes="context">Healthy</span>
|
||||||
|
</Slot>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
@TooltipText
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Slot>
|
||||||
|
<span class="text-destructive" @attributes="context">Unhealthy</span>
|
||||||
|
</Slot>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
@TooltipText
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public NodeDto Node { get; set; }
|
||||||
|
|
||||||
|
private bool IsLoading = true;
|
||||||
|
|
||||||
|
private string TooltipText = "An unknown error has occured. Check logs";
|
||||||
|
private bool IsHealthy;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!firstRender)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await HttpClient.GetFromJsonAsync<NodeHealthDto>($"api/admin/servers/nodes/{Node.Id}/health");
|
||||||
|
|
||||||
|
if(result == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IsHealthy = result.IsHealthy;
|
||||||
|
|
||||||
|
if (IsHealthy)
|
||||||
|
TooltipText = "Version: v2.1.0"; // TODO: Add version loading
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (result.StatusCode != 0)
|
||||||
|
{
|
||||||
|
if (result.StatusCode is >= 200 and <= 299)
|
||||||
|
{
|
||||||
|
if (result.RemoteStatusCode != 0)
|
||||||
|
{
|
||||||
|
TooltipText = result.RemoteStatusCode switch
|
||||||
|
{
|
||||||
|
401 => "Daemon is unable to authenticate against the panel",
|
||||||
|
404 => "Daemon is unable to request the panel's endpoint",
|
||||||
|
500 => "Panel encountered an internal server error",
|
||||||
|
_ => $"Panel returned {result.RemoteStatusCode}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
TooltipText = "Daemon is unable to reach the panel";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TooltipText = result.StatusCode switch
|
||||||
|
{
|
||||||
|
401 => "Panel is unable to authenticate against the node",
|
||||||
|
404 => "Panel is unable to request the daemon's endpoint",
|
||||||
|
500 => "Daemon encountered an internal server error",
|
||||||
|
_ => $"Daemon returned {result.StatusCode}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
TooltipText = "Moonlight is unable to reach the node";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "An unhandled error occured while fetching the node health status");
|
||||||
|
}
|
||||||
|
|
||||||
|
IsLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
154
MoonlightServers.Frontend/Admin/Nodes/Overview.razor
Normal file
154
MoonlightServers.Frontend/Admin/Nodes/Overview.razor
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Shared.Http.Requests
|
||||||
|
@using Moonlight.Shared.Http.Responses
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Nodes
|
||||||
|
@using ShadcnBlazor.DataGrids
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Dropdowns
|
||||||
|
@using ShadcnBlazor.Extras.AlertDialogs
|
||||||
|
@using ShadcnBlazor.Extras.Dialogs
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Tabels
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject AlertDialogService AlertDialogService
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject IAuthorizationService AuthorizationService
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between mt-5">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-xl font-semibold">Nodes</h1>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Manage nodes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-x-1.5">
|
||||||
|
<Button>
|
||||||
|
<Slot Context="buttonCtx">
|
||||||
|
<a @attributes="buttonCtx" href="/admin/servers/nodes/create"
|
||||||
|
data-disabled="@(!CreateAccess.Succeeded)">
|
||||||
|
<PlusIcon/>
|
||||||
|
Create
|
||||||
|
</a>
|
||||||
|
</Slot>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<DataGrid @ref="Grid" TGridItem="NodeDto" Loader="LoadAsync" PageSize="10" ClassName="bg-card">
|
||||||
|
<PropertyColumn Field="u => u.Id"/>
|
||||||
|
<TemplateColumn IsFilterable="true" Identifier="@nameof(NodeDto.Name)" Title="Name">
|
||||||
|
<CellTemplate>
|
||||||
|
<TableCell>
|
||||||
|
<a class="text-primary" href="#"
|
||||||
|
@onclick="() => Edit(context)" @onclick:preventDefault>
|
||||||
|
@context.Name
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn IsFilterable="false" Title="Status">
|
||||||
|
<CellTemplate>
|
||||||
|
<TableCell>
|
||||||
|
<NodeHealthDisplay Node="context" />
|
||||||
|
</TableCell>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Title="HTTP Endpoint"
|
||||||
|
Field="u => u.HttpEndpointUrl"/>
|
||||||
|
<TemplateColumn>
|
||||||
|
<CellTemplate>
|
||||||
|
<TableCell>
|
||||||
|
<div class="flex flex-row items-center justify-end me-3">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Slot Context="dropdownSlot">
|
||||||
|
<Button Size="ButtonSize.IconSm" Variant="ButtonVariant.Ghost"
|
||||||
|
@attributes="dropdownSlot">
|
||||||
|
<EllipsisIcon/>
|
||||||
|
</Button>
|
||||||
|
</Slot>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent SideOffset="2">
|
||||||
|
<DropdownMenuItem OnClick="() => Edit(context)"
|
||||||
|
Disabled="@(!EditAccess.Succeeded)">
|
||||||
|
Edit
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
<PenIcon/>
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem OnClick="() => DeleteAsync(context)"
|
||||||
|
Variant="DropdownMenuItemVariant.Destructive"
|
||||||
|
Disabled="@(!DeleteAccess.Succeeded)">
|
||||||
|
Delete
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
<TrashIcon/>
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||||
|
|
||||||
|
private DataGrid<NodeDto> Grid;
|
||||||
|
|
||||||
|
private AuthorizationResult EditAccess;
|
||||||
|
private AuthorizationResult DeleteAccess;
|
||||||
|
private AuthorizationResult CreateAccess;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var authState = await AuthState;
|
||||||
|
|
||||||
|
EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.Edit);
|
||||||
|
DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.Delete);
|
||||||
|
CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.Create);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DataGridResponse<NodeDto>> LoadAsync(DataGridRequest<NodeDto> request)
|
||||||
|
{
|
||||||
|
var query = $"?startIndex={request.StartIndex}&length={request.Length}";
|
||||||
|
var filterOptions = request.Filters.Count > 0 ? new FilterOptions(request.Filters) : null;
|
||||||
|
|
||||||
|
var response = await HttpClient.GetFromJsonAsync<PagedData<NodeDto>>(
|
||||||
|
$"api/admin/servers/nodes{query}&filterOptions={filterOptions}",
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
return new DataGridResponse<NodeDto>(response!.Data, response.TotalLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Edit(NodeDto context) => NavigationManager.NavigateTo($"/admin/servers/nodes/{context.Id}");
|
||||||
|
|
||||||
|
private async Task DeleteAsync(NodeDto context)
|
||||||
|
{
|
||||||
|
await AlertDialogService.ConfirmDangerAsync(
|
||||||
|
"Node Deletion",
|
||||||
|
$"Do you really want to delete the node {context.Name}? This cannot be undone.",
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var response = await HttpClient.DeleteAsync($"api/admin/servers/nodes/{context.Id}");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
await Grid.RefreshAsync();
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Node Deletion",
|
||||||
|
$"Successfully deleted node {context.Name}"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
@using LucideBlazor
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
@using ShadcnBlazor.Tabels
|
||||||
|
|
||||||
|
@inherits Editor<List<MoonlightServers.Shared.Admin.Templates.UpdateConfigurationFileMappingDto>>
|
||||||
|
|
||||||
|
<div class="rounded-md bg-card shadow-sm border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
Key
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
Value
|
||||||
|
</TableHead>
|
||||||
|
<TableHead ClassName="w-10"/>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
@foreach (var mapping in Value)
|
||||||
|
{
|
||||||
|
<TableRow @key="mapping">
|
||||||
|
<TableCell>
|
||||||
|
<TextInputField @bind-Value="mapping.Key"
|
||||||
|
placeholder="server-port"/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextInputField @bind-Value="mapping.Value"
|
||||||
|
placeholder="{{SERVER_PORT}}"/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell ClassName="text-right pr-4">
|
||||||
|
<Button @onclick="() => DeleteAsync(mapping)"
|
||||||
|
Size="ButtonSize.Icon"
|
||||||
|
Variant="ButtonVariant.Destructive">
|
||||||
|
<Trash2Icon/>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan="999999">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button
|
||||||
|
@onclick="AddAsync"
|
||||||
|
Variant="ButtonVariant.Outline"
|
||||||
|
Size="ButtonSize.Sm">
|
||||||
|
<PlusIcon/>
|
||||||
|
Add Mapping
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private async Task DeleteAsync(UpdateConfigurationFileMappingDto mappingDto)
|
||||||
|
{
|
||||||
|
Value.Remove(mappingDto);
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddAsync()
|
||||||
|
{
|
||||||
|
Value.Add(new UpdateConfigurationFileMappingDto()
|
||||||
|
{
|
||||||
|
Key = "Change me",
|
||||||
|
Value = "{{CHANGE_ME}}"
|
||||||
|
});
|
||||||
|
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
@using LucideBlazor
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Accordions
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
|
||||||
|
<div class="mb-5 flex justify-end">
|
||||||
|
<Button @onclick="Add">
|
||||||
|
<PlusIcon/>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
@foreach (var file in FilesConfig.ConfigurationFiles)
|
||||||
|
{
|
||||||
|
<Accordion ClassName="-space-y-px" Type="AccordionType.Single">
|
||||||
|
<AccordionItem
|
||||||
|
ClassName="overflow-hidden border bg-card px-4 first:rounded-t-lg last:rounded-b-lg last:border-b"
|
||||||
|
Value="element">
|
||||||
|
<AccordionTrigger ClassName="hover:no-underline">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<FileCogIcon/>
|
||||||
|
<span class="text-left">@file.Path</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent ClassName="ps-7">
|
||||||
|
<FieldGroup>
|
||||||
|
<FieldSet>
|
||||||
|
<Field>
|
||||||
|
@{
|
||||||
|
var id = $"configFilePath{file.GetHashCode()}";
|
||||||
|
}
|
||||||
|
<FieldLabel for="@id">Path</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="file.Path"
|
||||||
|
id="@id"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
@{
|
||||||
|
var id = $"configFileParser{file.GetHashCode()}";
|
||||||
|
}
|
||||||
|
<FieldLabel for="@id">Parser</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="file.Parser"
|
||||||
|
id="@id"/>
|
||||||
|
</Field>
|
||||||
|
</FieldSet>
|
||||||
|
<FieldSet>
|
||||||
|
<ConfigFileMappingEditor @bind-Value="file.Mappings" />
|
||||||
|
</FieldSet>
|
||||||
|
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||||
|
<Button Variant="ButtonVariant.Destructive"
|
||||||
|
@onclick="() => Delete(file)">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public UpdateFilesConfigDto FilesConfig { get; set; }
|
||||||
|
|
||||||
|
private void Add()
|
||||||
|
=> FilesConfig.ConfigurationFiles.Add(new());
|
||||||
|
|
||||||
|
private void Delete(UpdateConfigurationFileDto dto)
|
||||||
|
=> FilesConfig.ConfigurationFiles.Remove(dto);
|
||||||
|
}
|
||||||
132
MoonlightServers.Frontend/Admin/Templates/Create.razor
Normal file
132
MoonlightServers.Frontend/Admin/Templates/Create.razor
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
@page "/admin/servers/templates/create"
|
||||||
|
|
||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Cards
|
||||||
|
@using ShadcnBlazor.Extras.Forms
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
@using ShadcnBlazor.Tab
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-xl font-semibold">Create Template</h1>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Create a new template
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-x-1.5">
|
||||||
|
<Button Variant="ButtonVariant.Secondary">
|
||||||
|
<Slot>
|
||||||
|
<a href="/admin/servers?tab=templates" @attributes="context">
|
||||||
|
<ChevronLeftIcon/>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
</Slot>
|
||||||
|
</Button>
|
||||||
|
<SubmitButton>
|
||||||
|
<CheckIcon/>
|
||||||
|
Continue
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<FieldGroup>
|
||||||
|
<FormValidationSummary/>
|
||||||
|
|
||||||
|
<FieldSet>
|
||||||
|
<FieldGroup>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateName">Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.Name"
|
||||||
|
id="templateName"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateAuthor">Author</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.Author"
|
||||||
|
id="templateAuthor"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateVersion">Version</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.Version"
|
||||||
|
id="templateVersion"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
|
||||||
|
<FieldLabel for="templateDescription">Description</FieldLabel>
|
||||||
|
<TextareaInputField
|
||||||
|
@bind-Value="Request.Description"
|
||||||
|
id="templateDescription"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateDonateUrl">Donate URL</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.DonateUrl"
|
||||||
|
id="templateDonateUrl"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateUpdateUrl">Update URL</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.UpdateUrl"
|
||||||
|
id="templateUpdateUrl"/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</FieldGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</EnhancedEditForm>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private CreateTemplateDto Request = new();
|
||||||
|
|
||||||
|
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.PostAsJsonAsync(
|
||||||
|
"/api/admin/servers/templates",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Template Creation",
|
||||||
|
$"Successfully created template {Request.Name}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Navigation.NavigateTo("/admin/servers?tab=templates");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Dialogs
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Extras.Forms
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
@using ShadcnBlazor.Switches
|
||||||
|
|
||||||
|
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create Docker Image</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new docker image
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<EnhancedEditForm @ref="Form" Model="Model" OnValidSubmit="OnValidSubmitAsync">
|
||||||
|
<FieldGroup>
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
<FormValidationSummary/>
|
||||||
|
|
||||||
|
<FieldSet>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="dockerImageDisplayName">Display Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Model.DisplayName"
|
||||||
|
id="dockerImageDisplayName"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="dockerImageIdentifier">Image Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Model.ImageName"
|
||||||
|
id="dockerImageIdentifier"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="dockerImageSkipPulling">Skip Pulling</FieldLabel>
|
||||||
|
<Switch id="dockerImageSkipPulling" @bind-Value="Model.SkipPulling" />
|
||||||
|
</Field>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</EnhancedEditForm>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose>
|
||||||
|
<Slot>
|
||||||
|
<Button Type="button" Variant="ButtonVariant.Outline" @attributes="context">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Slot>
|
||||||
|
</DialogClose>
|
||||||
|
<Button @onclick="() => Form.SubmitAsync()" Type="button">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||||
|
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||||
|
|
||||||
|
private CreateDockerImageDto Model = new();
|
||||||
|
private EnhancedEditForm Form;
|
||||||
|
|
||||||
|
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.PostAsJsonAsync(
|
||||||
|
$"api/admin/servers/templates/{Template.Id}/dockerImages",
|
||||||
|
Model,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Model, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await OnSubmit.Invoke();
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Docker Image Creation",
|
||||||
|
$"Successfully created variable {Model.DisplayName}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await CloseAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Dialogs
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Extras.Forms
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
|
||||||
|
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create Variable</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new variable
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<EnhancedEditForm @ref="Form" Model="Model" OnValidSubmit="OnValidSubmitAsync">
|
||||||
|
<FieldGroup>
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
<FormValidationSummary/>
|
||||||
|
|
||||||
|
<FieldSet>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="variableDisplayName">Display Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Model.DisplayName"
|
||||||
|
id="variableDisplayName"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="variableDescription">Description</FieldLabel>
|
||||||
|
<TextareaInputField
|
||||||
|
@bind-Value="Model.Description"
|
||||||
|
id="variableDescription"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="variableKey">Key</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Model.EnvName"
|
||||||
|
id="variableKey"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="variableDefaultValue">Default Value</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Model.DefaultValue"
|
||||||
|
id="variableDefaultValue"/>
|
||||||
|
</Field>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</EnhancedEditForm>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose>
|
||||||
|
<Slot>
|
||||||
|
<Button Type="button" Variant="ButtonVariant.Outline" @attributes="context">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Slot>
|
||||||
|
</DialogClose>
|
||||||
|
<Button @onclick="() => Form.SubmitAsync()" Type="button">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||||
|
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||||
|
|
||||||
|
private CreateVariableDto Model = new();
|
||||||
|
private EnhancedEditForm Form;
|
||||||
|
|
||||||
|
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.PostAsJsonAsync(
|
||||||
|
$"api/admin/servers/templates/{Template.Id}/variables",
|
||||||
|
Model,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Model, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await OnSubmit.Invoke();
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Variable Creation",
|
||||||
|
$"Successfully created variable {Model.DisplayName}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await CloseAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
@using System.Text.Json
|
||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Shared.Http.Responses
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Extras.Common
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Cards
|
||||||
|
@using ShadcnBlazor.Extras.AlertDialogs
|
||||||
|
@using ShadcnBlazor.Extras.Dialogs
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Selects
|
||||||
|
@using ShadcnBlazor.Switches
|
||||||
|
@using ShadcnBlazor.Tabels
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject AlertDialogService AlertDialogService
|
||||||
|
@inject DialogService DialogService
|
||||||
|
|
||||||
|
<LazyLoader @ref="LazyLoader" Load="LoadAsync">
|
||||||
|
<Card ClassName="mb-5">
|
||||||
|
<CardContent>
|
||||||
|
<FieldGroup>
|
||||||
|
<FieldSet>
|
||||||
|
<FieldGroup>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateAllowUserDockerImageChange">Allow User Docker Image Change
|
||||||
|
</FieldLabel>
|
||||||
|
<Switch
|
||||||
|
@bind-Value="Request.AllowUserDockerImageChange"
|
||||||
|
id="templateAllowUserDockerImageChange"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>Default Docker Image</FieldLabel>
|
||||||
|
<Select @bind-Value="DefaultDockerImageBinder">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue Placeholder="Select a docker image" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
@foreach (var (id, dockerImage) in DockerImages)
|
||||||
|
{
|
||||||
|
<SelectItem Value="@id.ToString()">
|
||||||
|
@dockerImage.DisplayName
|
||||||
|
</SelectItem>
|
||||||
|
}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</FieldGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div class="overflow-hidden rounded-md border bg-card shadow-sm">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
Display Name
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
Identifier
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
Skip Pulling
|
||||||
|
</TableHead>
|
||||||
|
<TableHead ClassName="w-10"/>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
@foreach (var (id, dockerImage) in DockerImages)
|
||||||
|
{
|
||||||
|
<TableRow
|
||||||
|
@key="dockerImage">
|
||||||
|
<TableCell>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="dockerImage.DisplayName"
|
||||||
|
placeholder="Default Image"/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="dockerImage.ImageName"
|
||||||
|
placeholder="debian:latest"/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Switch @bind-Value="dockerImage.SkipPulling"/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell ClassName="text-right pr-4">
|
||||||
|
<Button @onclick="() => UpdateAsync(id, dockerImage)"
|
||||||
|
Size="ButtonSize.Icon"
|
||||||
|
Variant="ButtonVariant.Outline">
|
||||||
|
<SaveIcon/>
|
||||||
|
</Button>
|
||||||
|
<Button @onclick="() => DeleteAsync(id, dockerImage)"
|
||||||
|
Size="ButtonSize.Icon"
|
||||||
|
Variant="ButtonVariant.Destructive">
|
||||||
|
<Trash2Icon/>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan="999999">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button
|
||||||
|
@onclick="AddAsync"
|
||||||
|
Variant="ButtonVariant.Outline"
|
||||||
|
Size="ButtonSize.Sm">
|
||||||
|
<PlusIcon/>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||||
|
[Parameter] public UpdateTemplateDto Request { get; set; }
|
||||||
|
|
||||||
|
private readonly List<(int, UpdateDockerImageDto)> DockerImages = new();
|
||||||
|
private LazyLoader LazyLoader;
|
||||||
|
|
||||||
|
private string? DefaultDockerImageBinder
|
||||||
|
{
|
||||||
|
get => Request.DefaultDockerImageId?.ToString();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (int.TryParse(value, out var intValue))
|
||||||
|
Request.DefaultDockerImageId = intValue;
|
||||||
|
else
|
||||||
|
Request.DefaultDockerImageId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync(LazyLoader _)
|
||||||
|
{
|
||||||
|
DockerImages.Clear();
|
||||||
|
|
||||||
|
var totalAmount = 0;
|
||||||
|
var currentIndex = 0;
|
||||||
|
const int pageSize = 50;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var dockerImages = await HttpClient.GetFromJsonAsync<PagedData<DockerImageDto>>(
|
||||||
|
$"api/admin/servers/templates/{Template.Id}/dockerImages?startIndex={currentIndex}&length={pageSize}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dockerImages == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
currentIndex += dockerImages.Data.Length;
|
||||||
|
totalAmount = dockerImages.TotalLength;
|
||||||
|
|
||||||
|
DockerImages.AddRange(dockerImages.Data.Select(x => (x.Id, new UpdateDockerImageDto()
|
||||||
|
{
|
||||||
|
DisplayName = x.DisplayName,
|
||||||
|
ImageName = x.ImageName,
|
||||||
|
SkipPulling = x.SkipPulling
|
||||||
|
})));
|
||||||
|
} while (DockerImages.Count < totalAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteAsync(int id, UpdateDockerImageDto dto)
|
||||||
|
{
|
||||||
|
await AlertDialogService.ConfirmDangerAsync(
|
||||||
|
"Docker Image Deletion",
|
||||||
|
$"Do you really want to delete the docker image {dto.DisplayName}? This cannot be undone",
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{Template.Id}/dockerImages/{id}");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Docker Image Deletion",
|
||||||
|
$"Successfully deleted docker image {dto.DisplayName}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await LazyLoader.ReloadAsync();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateAsync(int id, UpdateDockerImageDto dto)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.PutAsJsonAsync(
|
||||||
|
$"api/admin/servers/templates/{Template.Id}/dockerImages/{id}",
|
||||||
|
dto,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(
|
||||||
|
Moonlight.Shared.Http.SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (problemDetails == null)
|
||||||
|
{
|
||||||
|
// Fallback
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (problemDetails.Errors is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var errorMessages = string.Join(
|
||||||
|
", ",
|
||||||
|
problemDetails.Errors.Select(x => $"{x.Key}: {x.Value.FirstOrDefault()}")
|
||||||
|
);
|
||||||
|
|
||||||
|
await ToastService.ErrorAsync(
|
||||||
|
"Docker Image Update",
|
||||||
|
$"{problemDetails.Detail ?? problemDetails.Title} {errorMessages}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ToastService.ErrorAsync(
|
||||||
|
"Docker Image Update",
|
||||||
|
$"{problemDetails.Detail ?? problemDetails.Title}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// Fallback if unable to deserialize
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Docker Image Update",
|
||||||
|
$"Successfully updated docker image {dto.DisplayName}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await LazyLoader.ReloadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddAsync()
|
||||||
|
{
|
||||||
|
await DialogService.LaunchAsync<CreateDockerImageDialog>(parameters =>
|
||||||
|
{
|
||||||
|
parameters[nameof(CreateDockerImageDialog.Template)] = Template;
|
||||||
|
parameters[nameof(CreateDockerImageDialog.OnSubmit)] = async () => { await LazyLoader.ReloadAsync(); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
@using LucideBlazor
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
@using ShadcnBlazor.Tabels
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
|
||||||
|
@inherits Editor<List<string>>
|
||||||
|
|
||||||
|
<div class="rounded-md bg-card shadow-sm border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
Online Text
|
||||||
|
</TableHead>
|
||||||
|
<TableHead ClassName="w-10"/>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
@foreach (var command in Value)
|
||||||
|
{
|
||||||
|
<TableRow
|
||||||
|
@key="command">
|
||||||
|
<TableCell>
|
||||||
|
<TextInputField Value="@command"
|
||||||
|
ValueExpression="() => command" disabled/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell ClassName="text-right pr-4">
|
||||||
|
<Button @onclick="() => DeleteAsync(command)"
|
||||||
|
Size="ButtonSize.Icon"
|
||||||
|
Variant="ButtonVariant.Destructive">
|
||||||
|
<Trash2Icon/>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan="999999">
|
||||||
|
<div class="flex justify-end gap-1">
|
||||||
|
<TextInputField ClassName="h-8"
|
||||||
|
@bind-Value="Input"
|
||||||
|
placeholder="Enter text..." />
|
||||||
|
<Button
|
||||||
|
@onclick="AddAsync"
|
||||||
|
Variant="ButtonVariant.Outline"
|
||||||
|
Size="ButtonSize.Sm">
|
||||||
|
<PlusIcon/>
|
||||||
|
Add Online Text
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private string Input;
|
||||||
|
|
||||||
|
private async Task DeleteAsync(string command)
|
||||||
|
{
|
||||||
|
Value.Remove(command);
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddAsync()
|
||||||
|
{
|
||||||
|
if(string.IsNullOrEmpty(Input))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(Value.Contains(Input))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Value.Add(Input);
|
||||||
|
Input = "";
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
202
MoonlightServers.Frontend/Admin/Templates/Overview.razor
Normal file
202
MoonlightServers.Frontend/Admin/Templates/Overview.razor
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Shared.Http.Requests
|
||||||
|
@using Moonlight.Shared.Http.Responses
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.DataGrids
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Dropdowns
|
||||||
|
@using ShadcnBlazor.Extras.AlertDialogs
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Tabels
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject AlertDialogService AlertDialogService
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject IAuthorizationService AuthorizationService
|
||||||
|
|
||||||
|
<InputFile OnChange="OnFileSelectedAsync" id="import-template" class="hidden" multiple accept=".yml,.yaml,.json"/>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between mt-5">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-xl font-semibold">Templates</h1>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Manage templates
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-x-1.5">
|
||||||
|
<Button Variant="ButtonVariant.Outline">
|
||||||
|
<Slot>
|
||||||
|
<label for="import-template" @attributes="context">
|
||||||
|
<HardDriveUploadIcon/>
|
||||||
|
Import
|
||||||
|
</label>
|
||||||
|
</Slot>
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<Slot Context="buttonCtx">
|
||||||
|
<a @attributes="buttonCtx" href="/admin/servers/templates/create"
|
||||||
|
data-disabled="@(!CreateAccess.Succeeded)">
|
||||||
|
<PlusIcon/>
|
||||||
|
Create
|
||||||
|
</a>
|
||||||
|
</Slot>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<DataGrid @ref="Grid" TGridItem="TemplateDto" Loader="LoadAsync" PageSize="10" ClassName="bg-card">
|
||||||
|
<PropertyColumn Field="u => u.Id"/>
|
||||||
|
<TemplateColumn IsFilterable="true" Identifier="@nameof(TemplateDto.Name)" Title="@nameof(TemplateDto.Name)">
|
||||||
|
<CellTemplate>
|
||||||
|
<TableCell>
|
||||||
|
<a class="text-primary" href="#"
|
||||||
|
@onclick="() => Edit(context)" @onclick:preventDefault>
|
||||||
|
@context.Name
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="@nameof(TemplateDto.Description)" HeadClassName="hidden lg:table-cell">
|
||||||
|
<CellTemplate>
|
||||||
|
<TableCell ClassName="hidden lg:table-cell">
|
||||||
|
<div class="truncate max-w-md">@context.Description</div>
|
||||||
|
</TableCell>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Field="u => u.Author"
|
||||||
|
HeadClassName="hidden xl:table-cell"
|
||||||
|
CellClassName="hidden xl:table-cell"/>
|
||||||
|
<PropertyColumn Field="u => u.Version"
|
||||||
|
HeadClassName="hidden xl:table-cell"
|
||||||
|
CellClassName="hidden xl:table-cell"/>
|
||||||
|
<TemplateColumn>
|
||||||
|
<CellTemplate>
|
||||||
|
<TableCell>
|
||||||
|
<div class="flex flex-row items-center justify-end me-3">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Slot Context="dropdownSlot">
|
||||||
|
<Button Size="ButtonSize.IconSm" Variant="ButtonVariant.Ghost"
|
||||||
|
@attributes="dropdownSlot">
|
||||||
|
<EllipsisIcon/>
|
||||||
|
</Button>
|
||||||
|
</Slot>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent SideOffset="2">
|
||||||
|
<DropdownMenuItem OnClick="() => Export(context)">
|
||||||
|
Export
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
<HardDriveDownloadIcon/>
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem OnClick="() => Edit(context)"
|
||||||
|
Disabled="@(!EditAccess.Succeeded)">
|
||||||
|
Edit
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
<PenIcon/>
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem OnClick="() => DeleteAsync(context)"
|
||||||
|
Variant="DropdownMenuItemVariant.Destructive"
|
||||||
|
Disabled="@(!DeleteAccess.Succeeded)">
|
||||||
|
Delete
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
<TrashIcon/>
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||||
|
|
||||||
|
private DataGrid<TemplateDto> Grid;
|
||||||
|
|
||||||
|
private AuthorizationResult EditAccess;
|
||||||
|
private AuthorizationResult DeleteAccess;
|
||||||
|
private AuthorizationResult CreateAccess;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var authState = await AuthState;
|
||||||
|
|
||||||
|
EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Edit);
|
||||||
|
DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Delete);
|
||||||
|
CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Create);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DataGridResponse<TemplateDto>> LoadAsync(DataGridRequest<TemplateDto> request)
|
||||||
|
{
|
||||||
|
var query = $"?startIndex={request.StartIndex}&length={request.Length}";
|
||||||
|
var filterOptions = request.Filters.Count > 0 ? new FilterOptions(request.Filters) : null;
|
||||||
|
|
||||||
|
var response = await HttpClient.GetFromJsonAsync<PagedData<TemplateDto>>(
|
||||||
|
$"api/admin/servers/templates{query}&filterOptions={filterOptions}",
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
return new DataGridResponse<TemplateDto>(response!.Data, response.TotalLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Edit(TemplateDto context) => NavigationManager.NavigateTo($"/admin/servers/templates/{context.Id}");
|
||||||
|
|
||||||
|
private void Export(TemplateDto dto) => NavigationManager.NavigateTo($"api/admin/servers/templates/{dto.Id}/export", true);
|
||||||
|
|
||||||
|
private async Task DeleteAsync(TemplateDto context)
|
||||||
|
{
|
||||||
|
await AlertDialogService.ConfirmDangerAsync(
|
||||||
|
"Template Deletion",
|
||||||
|
$"Do you really want to delete the template {context.Name}? This cannot be undone.",
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{context.Id}");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
await Grid.RefreshAsync();
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Template Deletion",
|
||||||
|
$"Successfully deleted template {context.Name}"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnFileSelectedAsync(InputFileChangeEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
var files = eventArgs.GetMultipleFiles();
|
||||||
|
|
||||||
|
foreach (var browserFile in files)
|
||||||
|
{
|
||||||
|
await using var contentStream = browserFile.OpenReadStream(browserFile.Size);
|
||||||
|
|
||||||
|
var response = await HttpClient.PostAsync(
|
||||||
|
"api/admin/servers/templates/import",
|
||||||
|
new StreamContent(contentStream)
|
||||||
|
);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var importedTemplate = await response
|
||||||
|
.Content
|
||||||
|
.ReadFromJsonAsync<TemplateDto>(SerializationContext.Default.Options);
|
||||||
|
|
||||||
|
if (importedTemplate == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await Grid.RefreshAsync();
|
||||||
|
await ToastService.SuccessAsync("Template Import", $"Successfully imported template {importedTemplate.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
MoonlightServers.Frontend/Admin/Templates/ScriptEditor.razor
Normal file
16
MoonlightServers.Frontend/Admin/Templates/ScriptEditor.razor
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@using ShadcnBlazor.Extras.Editors
|
||||||
|
|
||||||
|
@inherits Editor<string>
|
||||||
|
|
||||||
|
<Editor @ref="Editor" Language="EditorLanguage.None" InitialValue="@Value" />
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private Editor Editor;
|
||||||
|
|
||||||
|
public async Task SubmitAsync()
|
||||||
|
{
|
||||||
|
Value = await Editor.GetValueAsync();
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
@using LucideBlazor
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
@using ShadcnBlazor.Tabels
|
||||||
|
|
||||||
|
@inherits Editor<List<MoonlightServers.Shared.Admin.Templates.UpdateStartupCommandDto>>
|
||||||
|
|
||||||
|
<div class="rounded-md bg-card shadow-sm border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
Display Name
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
Command
|
||||||
|
</TableHead>
|
||||||
|
<TableHead ClassName="w-10"/>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
@foreach (var command in Value)
|
||||||
|
{
|
||||||
|
<TableRow
|
||||||
|
@key="command">
|
||||||
|
<TableCell>
|
||||||
|
<TextInputField @bind-Value="command.DisplayName"
|
||||||
|
placeholder="Default Command"/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextInputField @bind-Value="command.Command"
|
||||||
|
placeholder="java -Xmx{{SERVER_MEMORY}} server.jar"/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell ClassName="text-right pr-4">
|
||||||
|
<Button @onclick="() => DeleteAsync(command)"
|
||||||
|
Size="ButtonSize.Icon"
|
||||||
|
Variant="ButtonVariant.Destructive">
|
||||||
|
<Trash2Icon/>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan="999999">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button
|
||||||
|
@onclick="AddAsync"
|
||||||
|
Variant="ButtonVariant.Outline"
|
||||||
|
Size="ButtonSize.Sm">
|
||||||
|
<PlusIcon/>
|
||||||
|
Add Startup Command
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private async Task DeleteAsync(UpdateStartupCommandDto commandDto)
|
||||||
|
{
|
||||||
|
Value.Remove(commandDto);
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddAsync()
|
||||||
|
{
|
||||||
|
Value.Add(new UpdateStartupCommandDto()
|
||||||
|
{
|
||||||
|
Command = "Change me",
|
||||||
|
DisplayName = "Change me"
|
||||||
|
});
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
MoonlightServers.Frontend/Admin/Templates/TemplateMapper.cs
Normal file
13
MoonlightServers.Frontend/Admin/Templates/TemplateMapper.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using MoonlightServers.Shared.Admin.Templates;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Frontend.Admin.Templates;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||||
|
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||||
|
public static partial class TemplateMapper
|
||||||
|
{
|
||||||
|
public static partial UpdateTemplateDto ToRequest(DetailedTemplateDto dto);
|
||||||
|
}
|
||||||
297
MoonlightServers.Frontend/Admin/Templates/Update.razor
Normal file
297
MoonlightServers.Frontend/Admin/Templates/Update.razor
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
@page "/admin/servers/templates/{Id:int}"
|
||||||
|
|
||||||
|
@using System.Net
|
||||||
|
@using System.Text.Json
|
||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Cards
|
||||||
|
@using ShadcnBlazor.Emptys
|
||||||
|
@using ShadcnBlazor.Extras.Common
|
||||||
|
@using ShadcnBlazor.Extras.Forms
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
@using ShadcnBlazor.Tab
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<LazyLoader Load="LoadAsync">
|
||||||
|
@if (Template == null)
|
||||||
|
{
|
||||||
|
<Empty>
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||||
|
<SearchIcon/>
|
||||||
|
</EmptyMedia>
|
||||||
|
<EmptyTitle>Template not found</EmptyTitle>
|
||||||
|
<EmptyDescription>
|
||||||
|
A template with this id cannot be found
|
||||||
|
</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
</Empty>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-xl font-semibold">Update Template</h1>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Update template @Template.Name
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-x-1.5">
|
||||||
|
<Button Variant="ButtonVariant.Secondary">
|
||||||
|
<Slot>
|
||||||
|
<a href="/admin/servers?tab=templates" @attributes="context">
|
||||||
|
<ChevronLeftIcon/>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
</Slot>
|
||||||
|
</Button>
|
||||||
|
<SubmitButton>
|
||||||
|
<CheckIcon/>
|
||||||
|
Continue
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 space-y-5">
|
||||||
|
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
|
||||||
|
<FormValidationSummary/>
|
||||||
|
|
||||||
|
<Tabs DefaultValue="meta">
|
||||||
|
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
|
||||||
|
<TabsTrigger Value="meta">
|
||||||
|
<IdCardIcon/>
|
||||||
|
Meta
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger Value="lifecycle">
|
||||||
|
<PlayIcon/>
|
||||||
|
Lifecycle
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger Value="installation">
|
||||||
|
<SquareTerminalIcon/>
|
||||||
|
Installation
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger Value="variables">
|
||||||
|
<VariableIcon/>
|
||||||
|
Variables
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger Value="dockerImages">
|
||||||
|
<ContainerIcon/>
|
||||||
|
Docker Images
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger Value="files">
|
||||||
|
<FileCogIcon/>
|
||||||
|
Files
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent Value="meta">
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<FieldGroup>
|
||||||
|
<FieldSet>
|
||||||
|
<FieldGroup>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateName">Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.Name"
|
||||||
|
id="templateName"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateAuthor">Author</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.Author"
|
||||||
|
id="templateAuthor"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateVersion">Version</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.Version"
|
||||||
|
id="templateVersion"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
|
||||||
|
<FieldLabel for="templateDescription">Description</FieldLabel>
|
||||||
|
<TextareaInputField
|
||||||
|
@bind-Value="Request.Description"
|
||||||
|
id="templateDescription"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateDonateUrl">Donate URL</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.DonateUrl"
|
||||||
|
id="templateDonateUrl"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateUpdateUrl">Update URL</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.UpdateUrl"
|
||||||
|
id="templateUpdateUrl"/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</FieldGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent Value="lifecycle">
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<FieldGroup>
|
||||||
|
<FieldSet>
|
||||||
|
<FieldGroup>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
|
<Field ClassName="col-span-1 lg:col-span-2">
|
||||||
|
<FieldLabel>Startup Commands</FieldLabel>
|
||||||
|
<StartupCommandEditor
|
||||||
|
@bind-Value="Request.LifecycleConfig.StartupCommands"/>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateStopCommand">Stop Command</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.LifecycleConfig.StopCommand"
|
||||||
|
id="templateStopCommand"/>
|
||||||
|
</Field>
|
||||||
|
<Field ClassName="col-span-1 lg:col-span-2">
|
||||||
|
<FieldLabel>Online Texts</FieldLabel>
|
||||||
|
<OnlineTextEditor
|
||||||
|
@bind-Value="Request.LifecycleConfig.OnlineLogPatterns"/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</FieldGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent Value="installation">
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<FieldGroup>
|
||||||
|
<FieldSet>
|
||||||
|
<FieldGroup>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateInstallDockerImage">Docker Image
|
||||||
|
</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.InstallationConfig.DockerImage"
|
||||||
|
id="templateInstallDockerImage"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel for="templateInstallShell">Shell</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.InstallationConfig.Shell"
|
||||||
|
id="templateInstallShell"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
|
||||||
|
<FieldLabel>Script</FieldLabel>
|
||||||
|
<ScriptEditor
|
||||||
|
@ref="ScriptEditor"
|
||||||
|
@bind-Value="Request.InstallationConfig.Script"/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</FieldGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</FieldGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent Value="variables">
|
||||||
|
<VariableListEditor Template="Template"/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent Value="dockerImages">
|
||||||
|
<DockerImageListEditor Request="Request" Template="Template"/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent Value="files">
|
||||||
|
<ConfigFilesEditor FilesConfig="Request.FilesConfig"/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</EnhancedEditForm>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public int Id { get; set; }
|
||||||
|
|
||||||
|
private UpdateTemplateDto Request;
|
||||||
|
private DetailedTemplateDto? Template;
|
||||||
|
|
||||||
|
private ScriptEditor ScriptEditor;
|
||||||
|
|
||||||
|
private async Task LoadAsync(LazyLoader _)
|
||||||
|
{
|
||||||
|
// Meta
|
||||||
|
var metaResponse = await HttpClient.GetAsync($"api/admin/servers/templates/{Id}");
|
||||||
|
|
||||||
|
if (!metaResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
if (metaResponse.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
return;
|
||||||
|
|
||||||
|
metaResponse.EnsureSuccessStatusCode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Template = await metaResponse.Content.ReadFromJsonAsync<DetailedTemplateDto>(
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Template == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Request = TemplateMapper.ToRequest(Template);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||||
|
{
|
||||||
|
await ScriptEditor.SubmitAsync();
|
||||||
|
|
||||||
|
var response = await HttpClient.PutAsJsonAsync(
|
||||||
|
$"/api/admin/servers/templates/{Id}",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Template Update",
|
||||||
|
$"Successfully updated template {Request.Name}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Navigation.NavigateTo("/admin/servers?tab=templates");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
147
MoonlightServers.Frontend/Admin/Templates/VariableEditor.razor
Normal file
147
MoonlightServers.Frontend/Admin/Templates/VariableEditor.razor
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using MoonlightServers.Shared
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Accordions
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Extras.AlertDialogs
|
||||||
|
@using ShadcnBlazor.Extras.Forms
|
||||||
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject AlertDialogService AlertDialogService
|
||||||
|
|
||||||
|
<Accordion ClassName="-space-y-px" Type="AccordionType.Single">
|
||||||
|
<AccordionItem
|
||||||
|
ClassName="overflow-hidden border bg-card px-4 first:rounded-t-lg last:rounded-b-lg last:border-b"
|
||||||
|
Value="element">
|
||||||
|
<AccordionTrigger ClassName="hover:no-underline">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<VariableIcon/>
|
||||||
|
<span class="text-left">@Request.DisplayName</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent ClassName="ps-7">
|
||||||
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnValidSubmitAsync">
|
||||||
|
<FieldGroup>
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
<FormValidationSummary/>
|
||||||
|
|
||||||
|
<FieldSet>
|
||||||
|
<Field>
|
||||||
|
@{
|
||||||
|
var id = $"variableDisplayName{Variable.Id}";
|
||||||
|
}
|
||||||
|
<FieldLabel for="@id">Display Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.DisplayName"
|
||||||
|
id="@id"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
@{
|
||||||
|
var id = $"variableDescription{Variable.Id}";
|
||||||
|
}
|
||||||
|
<FieldLabel for="@id">Description</FieldLabel>
|
||||||
|
<TextareaInputField
|
||||||
|
@bind-Value="Request.Description"
|
||||||
|
id="@id"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
@{
|
||||||
|
var id = $"variableKey{Variable.Id}";
|
||||||
|
}
|
||||||
|
<FieldLabel for="@id">Key</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.EnvName"
|
||||||
|
id="@id"/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
@{
|
||||||
|
var id = $"variableDefaultValue{Variable.Id}";
|
||||||
|
}
|
||||||
|
<FieldLabel for="@id">Default Value</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
|
@bind-Value="Request.DefaultValue"
|
||||||
|
id="@id"/>
|
||||||
|
</Field>
|
||||||
|
</FieldSet>
|
||||||
|
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||||
|
<Button Variant="ButtonVariant.Destructive" @onclick="DeleteAsync">Delete</Button>
|
||||||
|
<SubmitButton>Save changes</SubmitButton>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</EnhancedEditForm>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public VariableDto Variable { get; set; }
|
||||||
|
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||||
|
[Parameter] public Func<Task> OnChanged { get; set; }
|
||||||
|
|
||||||
|
private UpdateVariableDto Request;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Request = new UpdateVariableDto()
|
||||||
|
{
|
||||||
|
DisplayName = Variable.DisplayName,
|
||||||
|
DefaultValue = Variable.DefaultValue,
|
||||||
|
EnvName = Variable.EnvName,
|
||||||
|
Description = Variable.Description
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.PutAsJsonAsync(
|
||||||
|
$"api/admin/servers/templates/{Template.Id}/variables/{Variable.Id}",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Variable Update",
|
||||||
|
$"Successfully updated variable {Request.DisplayName}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await OnChanged.Invoke();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteAsync()
|
||||||
|
{
|
||||||
|
await AlertDialogService.ConfirmDangerAsync(
|
||||||
|
"Variable Deletion",
|
||||||
|
$"Do you really want to delete the variable {Variable.DisplayName}? This cannot be undone",
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{Template.Id}/variables/{Variable.Id}");
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"Variable Deletion",
|
||||||
|
$"Variable {Variable.DisplayName} successfully deleted"
|
||||||
|
);
|
||||||
|
|
||||||
|
await OnChanged.Invoke();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Shared.Http.Responses
|
||||||
|
@using MoonlightServers.Shared.Admin.Templates
|
||||||
|
@using ShadcnBlazor.Accordions
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
|
@using ShadcnBlazor.Extras.Common
|
||||||
|
@using ShadcnBlazor.Extras.Dialogs
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
|
@using ShadcnBlazor.Inputs
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject DialogService DialogService
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end mb-5">
|
||||||
|
<Button @onclick="LaunchCreateAsync">
|
||||||
|
<PlusIcon />
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LazyLoader @ref="LazyLoader" Load="LoadAsync">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
|
@foreach (var variable in Variables)
|
||||||
|
{
|
||||||
|
<VariableEditor OnChanged="() => LazyLoader.ReloadAsync()"
|
||||||
|
Template="Template"
|
||||||
|
Variable="variable"
|
||||||
|
@key="variable" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||||
|
|
||||||
|
private readonly List<VariableDto> Variables = new();
|
||||||
|
private LazyLoader LazyLoader;
|
||||||
|
|
||||||
|
private async Task LoadAsync(LazyLoader _)
|
||||||
|
{
|
||||||
|
Variables.Clear();
|
||||||
|
|
||||||
|
var totalAmount = 0;
|
||||||
|
var currentIndex = 0;
|
||||||
|
const int pageSize = 50;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var variables = await HttpClient.GetFromJsonAsync<PagedData<VariableDto>>(
|
||||||
|
$"api/admin/servers/templates/{Template.Id}/variables?startIndex={currentIndex}&length={pageSize}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variables == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
currentIndex += variables.Data.Length;
|
||||||
|
totalAmount = variables.TotalLength;
|
||||||
|
|
||||||
|
Variables.AddRange(variables.Data);
|
||||||
|
|
||||||
|
} while (Variables.Count < totalAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LaunchCreateAsync()
|
||||||
|
{
|
||||||
|
await DialogService.LaunchAsync<CreateVariableDialog>(parameters =>
|
||||||
|
{
|
||||||
|
parameters[nameof(CreateVariableDialog.Template)] = Template;
|
||||||
|
parameters[nameof(CreateVariableDialog.OnSubmit)] = async () =>
|
||||||
|
{
|
||||||
|
await LazyLoader.ReloadAsync();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using LucideBlazor;
|
|
||||||
using Moonlight.Frontend.Interfaces;
|
|
||||||
using Moonlight.Frontend.Models;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Frontend.Implementations;
|
|
||||||
|
|
||||||
public class PermissionProvider : IPermissionProvider
|
|
||||||
{
|
|
||||||
public Task<PermissionCategory[]> GetPermissionsAsync()
|
|
||||||
{
|
|
||||||
return Task.FromResult<PermissionCategory[]>([
|
|
||||||
new PermissionCategory("Demo", typeof(SparklesIcon), [
|
|
||||||
new Permission("Permissions:Demo", "Demo", "Access to demo page")
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using LucideBlazor;
|
|
||||||
using Moonlight.Frontend.Interfaces;
|
|
||||||
using Moonlight.Frontend.Models;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Frontend.Implementations;
|
|
||||||
|
|
||||||
public sealed class SidebarProvider : ISidebarProvider
|
|
||||||
{
|
|
||||||
public Task<SidebarItem[]> GetItemsAsync()
|
|
||||||
{
|
|
||||||
return Task.FromResult<SidebarItem[]>([
|
|
||||||
new SidebarItem()
|
|
||||||
{
|
|
||||||
Group = "Demo",
|
|
||||||
Name = "Demo",
|
|
||||||
IconType = typeof(SparklesIcon),
|
|
||||||
Path = "/demo"
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using LucideBlazor;
|
||||||
|
using Moonlight.Frontend.Interfaces;
|
||||||
|
using Moonlight.Frontend.Models;
|
||||||
|
using MoonlightServers.Shared;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Frontend.Infrastructure;
|
||||||
|
|
||||||
|
public class PermissionProvider : IPermissionProvider
|
||||||
|
{
|
||||||
|
public Task<PermissionCategory[]> GetPermissionsAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult<PermissionCategory[]>([
|
||||||
|
new PermissionCategory("Servers - Nodes", typeof(ServerIcon), [
|
||||||
|
new Permission(Permissions.Nodes.View, "View", "Viewing all nodes"),
|
||||||
|
new Permission(Permissions.Nodes.Create, "Create", "Creating new nodes"),
|
||||||
|
new Permission(Permissions.Nodes.Edit, "Edit", "Editing nodes"),
|
||||||
|
new Permission(Permissions.Nodes.Delete, "Delete", "Deleting nodes"),
|
||||||
|
]),
|
||||||
|
new PermissionCategory("Servers - Servers", typeof(ContainerIcon), [
|
||||||
|
new Permission(Permissions.Servers.View, "View", "Viewing all servers"),
|
||||||
|
new Permission(Permissions.Servers.Create, "Create", "Creating new servers"),
|
||||||
|
new Permission(Permissions.Servers.Edit, "Edit", "Editing servers"),
|
||||||
|
new Permission(Permissions.Servers.Delete, "Delete", "Deleting servers"),
|
||||||
|
]),
|
||||||
|
new PermissionCategory("Servers - Templates", typeof(ContainerIcon), [
|
||||||
|
new Permission(Permissions.Templates.View, "View", "Viewing all templates"),
|
||||||
|
new Permission(Permissions.Templates.Create, "Create", "Creating new templates"),
|
||||||
|
new Permission(Permissions.Templates.Edit, "Edit", "Editing templates"),
|
||||||
|
new Permission(Permissions.Templates.Delete, "Delete", "Deleting templates"),
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
MoonlightServers.Frontend/Infrastructure/SidebarProvider.cs
Normal file
29
MoonlightServers.Frontend/Infrastructure/SidebarProvider.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using LucideBlazor;
|
||||||
|
using Moonlight.Frontend.Interfaces;
|
||||||
|
using Moonlight.Frontend.Models;
|
||||||
|
using MoonlightServers.Shared;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Frontend.Infrastructure;
|
||||||
|
|
||||||
|
public sealed class SidebarProvider : ISidebarProvider
|
||||||
|
{
|
||||||
|
public Task<SidebarItem[]> GetItemsAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult<SidebarItem[]>([
|
||||||
|
new SidebarItem()
|
||||||
|
{
|
||||||
|
Name = "Servers",
|
||||||
|
IconType = typeof(ServerIcon),
|
||||||
|
Path = "/servers"
|
||||||
|
},
|
||||||
|
new SidebarItem()
|
||||||
|
{
|
||||||
|
Group = "Admin",
|
||||||
|
Name = "Servers",
|
||||||
|
IconType = typeof(ServerIcon),
|
||||||
|
Path = "/admin/servers",
|
||||||
|
Policy = Permissions.Servers.View
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Moonlight.Frontend" Version="2.1.0"/>
|
<PackageReference Include="Moonlight.Frontend" Version="2.1.0"/>
|
||||||
|
<PackageReference Include="ShadcnBlazor" Version="1.0.14"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Client\" />
|
||||||
<Folder Include="wwwroot\"/>
|
<Folder Include="wwwroot\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Moonlight.Frontend;
|
using Moonlight.Frontend;
|
||||||
using Moonlight.Frontend.Configuration;
|
using Moonlight.Frontend.Configuration;
|
||||||
using Moonlight.Frontend.Interfaces;
|
using Moonlight.Frontend.Interfaces;
|
||||||
using MoonlightServers.Frontend.Implementations;
|
using MoonlightServers.Frontend.Infrastructure;
|
||||||
using SimplePlugin.Abstractions;
|
using SimplePlugin.Abstractions;
|
||||||
|
|
||||||
namespace MoonlightServers.Frontend;
|
namespace MoonlightServers.Frontend;
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
@using Moonlight.Frontend.Helpers
|
|
||||||
@using MoonlightServers.Shared
|
|
||||||
@using MoonlightServers.Shared.Http.Requests
|
|
||||||
@using MoonlightServers.Shared.Http.Responses
|
|
||||||
@using ShadcnBlazor.Buttons
|
|
||||||
@using ShadcnBlazor.Dialogs
|
|
||||||
@using ShadcnBlazor.Extras.AlertDialogs
|
|
||||||
@using ShadcnBlazor.Extras.Forms
|
|
||||||
@using ShadcnBlazor.Fields
|
|
||||||
@using ShadcnBlazor.Inputs
|
|
||||||
|
|
||||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
|
||||||
|
|
||||||
@inject HttpClient HttpClient
|
|
||||||
@inject AlertDialogService AlertDialogService
|
|
||||||
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Example Form</DialogTitle>
|
|
||||||
<DialogDescription>This forms removes all spaces from the input</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<EnhancedEditForm @ref="Form" OnValidSubmit="OnSubmit" Model="Dto">
|
|
||||||
<DataAnnotationsValidator/>
|
|
||||||
|
|
||||||
<FieldSet>
|
|
||||||
<FormValidationSummary/>
|
|
||||||
|
|
||||||
<FieldGroup>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel for="formInput">Form Input</FieldLabel>
|
|
||||||
<TextInputField id="formInput" @bind-Value="Dto.TextField"/>
|
|
||||||
<FieldDescription>Input you want to remove the spaces from</FieldDescription>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
</EnhancedEditForm>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button @onclick="() => Form.SubmitAsync()">Submit</Button>
|
|
||||||
<DialogClose/>
|
|
||||||
</DialogFooter>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
private FormSubmitDto Dto = new();
|
|
||||||
private EnhancedEditForm Form;
|
|
||||||
|
|
||||||
private async Task<bool> OnSubmit(EditContext editContext, ValidationMessageStore validationMessageStore)
|
|
||||||
{
|
|
||||||
var response = await HttpClient.PostAsJsonAsync(
|
|
||||||
"api/form",
|
|
||||||
Dto,
|
|
||||||
SerializationContext.Default.Options
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var data = await response.Content.ReadFromJsonAsync<FormResultDto>(
|
|
||||||
SerializationContext.Default.Options
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data == null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
await AlertDialogService.InfoAsync("Result", data.Result);
|
|
||||||
|
|
||||||
await CloseAsync();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Dto, validationMessageStore);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
@page "/demo"
|
|
||||||
@using LucideBlazor
|
|
||||||
@using MoonlightServers.Frontend.UI.Components
|
|
||||||
@using ShadcnBlazor.Buttons
|
|
||||||
@using ShadcnBlazor.Cards
|
|
||||||
@using ShadcnBlazor.Extras.Dialogs
|
|
||||||
|
|
||||||
@inject DialogService DialogService
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
|
||||||
<Card ClassName="col-span-1">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Demo</CardTitle>
|
|
||||||
<CardDescription>A cool demo page</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
You successfully used the plugin template to create your moonlight plugin :)
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<Button>
|
|
||||||
<Slot>
|
|
||||||
<a @attributes="context" href="https://moonlightpa.nl/dev">
|
|
||||||
<ExternalLinkIcon/>
|
|
||||||
Visit documentation
|
|
||||||
</a>
|
|
||||||
</Slot>
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent>
|
|
||||||
<Button @onclick="LaunchFormAsync" Variant="ButtonVariant.Outline">
|
|
||||||
Open Form
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
private async Task LaunchFormAsync()
|
|
||||||
=> await DialogService.LaunchAsync<FormDialog>();
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@using System.Net.Http
|
|
||||||
@using System.Net.Http.Json
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
|
||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
|
||||||
10
MoonlightServers.Frontend/_Imports.razor
Normal file
10
MoonlightServers.Frontend/_Imports.razor
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
14
MoonlightServers.Shared/Admin/Nodes/CreateNodeDto.cs
Normal file
14
MoonlightServers.Shared/Admin/Nodes/CreateNodeDto.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Nodes;
|
||||||
|
|
||||||
|
public class CreateNodeDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string HttpEndpointUrl { get; set; }
|
||||||
|
}
|
||||||
9
MoonlightServers.Shared/Admin/Nodes/NodeDto.cs
Normal file
9
MoonlightServers.Shared/Admin/Nodes/NodeDto.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Nodes;
|
||||||
|
|
||||||
|
public record NodeDto(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
string HttpEndpointUrl,
|
||||||
|
DateTimeOffset CreatedAt,
|
||||||
|
DateTimeOffset UpdatedAt
|
||||||
|
);
|
||||||
8
MoonlightServers.Shared/Admin/Nodes/NodeHealthDto.cs
Normal file
8
MoonlightServers.Shared/Admin/Nodes/NodeHealthDto.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Nodes;
|
||||||
|
|
||||||
|
public class NodeHealthDto
|
||||||
|
{
|
||||||
|
public int RemoteStatusCode { get; set; }
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
public bool IsHealthy { get; set; }
|
||||||
|
}
|
||||||
14
MoonlightServers.Shared/Admin/Nodes/UpdateNodeDto.cs
Normal file
14
MoonlightServers.Shared/Admin/Nodes/UpdateNodeDto.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Nodes;
|
||||||
|
|
||||||
|
public class UpdateNodeDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string HttpEndpointUrl { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class CreateDockerImageDto
|
||||||
|
{
|
||||||
|
[Required, MaxLength(30)]
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string ImageName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool SkipPulling { get; set; }
|
||||||
|
}
|
||||||
24
MoonlightServers.Shared/Admin/Templates/CreateTemplateDto.cs
Normal file
24
MoonlightServers.Shared/Admin/Templates/CreateTemplateDto.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class CreateTemplateDto
|
||||||
|
{
|
||||||
|
[Required, MaxLength(30)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(30)]
|
||||||
|
public string Author { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(30)]
|
||||||
|
public string Version { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(2048)]
|
||||||
|
public string? UpdateUrl { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(2048)]
|
||||||
|
public string? DonateUrl { get; set; }
|
||||||
|
}
|
||||||
18
MoonlightServers.Shared/Admin/Templates/CreateVariableDto.cs
Normal file
18
MoonlightServers.Shared/Admin/Templates/CreateVariableDto.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class CreateVariableDto
|
||||||
|
{
|
||||||
|
[Required, MaxLength(30)]
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(60)]
|
||||||
|
public string EnvName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(1024)]
|
||||||
|
public string? DefaultValue { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public record DetailedTemplateDto(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
string Author,
|
||||||
|
string Version,
|
||||||
|
string? UpdateUrl,
|
||||||
|
string? DonateUrl,
|
||||||
|
FilesConfigDto FilesConfig,
|
||||||
|
LifecycleConfigDto LifecycleConfig,
|
||||||
|
InstallationConfigDto InstallationConfig,
|
||||||
|
MiscellaneousConfigDto MiscellaneousConfig,
|
||||||
|
bool AllowUserDockerImageChange,
|
||||||
|
int? DefaultDockerImageId
|
||||||
|
);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public record DockerImageDto(
|
||||||
|
int Id,
|
||||||
|
string DisplayName,
|
||||||
|
string ImageName,
|
||||||
|
bool SkipPulling
|
||||||
|
);
|
||||||
20
MoonlightServers.Shared/Admin/Templates/FilesConfigDto.cs
Normal file
20
MoonlightServers.Shared/Admin/Templates/FilesConfigDto.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class FilesConfigDto
|
||||||
|
{
|
||||||
|
public List<ConfigurationFileDto> ConfigurationFiles { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigurationFileDto
|
||||||
|
{
|
||||||
|
public string Path { get; set; }
|
||||||
|
public string Parser { get; set; }
|
||||||
|
|
||||||
|
public List<ConfigurationFileMappingDto> Mappings { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigurationFileMappingDto
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string? Value { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public record InstallationConfigDto(string DockerImage, string Shell, string Script);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public record LifecycleConfigDto(StartupCommandDto[] StartupCommands, string StopCommand, string[] OnlineLogPatterns);
|
||||||
|
public record StartupCommandDto(string DisplayName, string Command);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public record MiscellaneousConfigDto(bool UseLegacyStartup);
|
||||||
11
MoonlightServers.Shared/Admin/Templates/TemplateDto.cs
Normal file
11
MoonlightServers.Shared/Admin/Templates/TemplateDto.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public record TemplateDto(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
string Author,
|
||||||
|
string Version,
|
||||||
|
string? UpdateUrl,
|
||||||
|
string? DonateUrl
|
||||||
|
);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class UpdateDockerImageDto
|
||||||
|
{
|
||||||
|
[Required, MaxLength(30)]
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string ImageName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool SkipPulling { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class UpdateFilesConfigDto
|
||||||
|
{
|
||||||
|
public List<UpdateConfigurationFileDto> ConfigurationFiles { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateConfigurationFileDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Parser { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<UpdateConfigurationFileMappingDto> Mappings { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateConfigurationFileMappingDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? Value { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class UpdateInstallationConfigDto
|
||||||
|
{
|
||||||
|
[Required, MaxLength(30)]
|
||||||
|
public string Shell { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string DockerImage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Script { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class UpdateLifecycleConfigDto
|
||||||
|
{
|
||||||
|
public List<UpdateStartupCommandDto> StartupCommands { get; set; } = [];
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string StopCommand { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> OnlineLogPatterns { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateStartupCommandDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Command { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MoonlightServers.Shared.Admin.Templates;
|
||||||
|
|
||||||
|
public class UpdateMiscellaneousConfigDto
|
||||||
|
{
|
||||||
|
public bool UseLegacyStartup { get; set; }
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user