Implemented template crud, db entities, import/export, ptero and pelican import
This commit is contained in:
@@ -8,9 +8,9 @@
|
||||
|
||||
<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>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.3" PrivateAssets="all"/>
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
|
||||
@source "../bin/Moonlight.Frontend/*.map";
|
||||
|
||||
@source "../../../Moonlight.Api/**/*.razor";
|
||||
@source "../../../Moonlight.Api/**/*.cs";
|
||||
@source "../../../Moonlight.Api/**/*.html";
|
||||
@source "../../../MoonlightServers.Api/**/*.razor";
|
||||
@source "../../../MoonlightServers.Api/**/*.cs";
|
||||
@source "../../../MoonlightServers.Api/**/*.html";
|
||||
|
||||
@source "../../../Moonlight.Frontend/**/*.razor";
|
||||
@source "../../../Moonlight.Frontend/**/*.cs";
|
||||
@source "../../../Moonlight.Frontend/**/*.html";
|
||||
@source "../../../MoonlightServers.Frontend/**/*.razor";
|
||||
@source "../../../MoonlightServers.Frontend/**/*.cs";
|
||||
@source "../../../MoonlightServers.Frontend/**/*.html";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ 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)
|
||||
@@ -34,5 +37,25 @@ public class DataContext : DbContext
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public class DbMigrationService : IHostedLifecycleService
|
||||
|
||||
if (migrationNames.Length == 0)
|
||||
{
|
||||
Logger.LogDebug("No pending migrations found");
|
||||
Logger.LogTrace("No pending migrations found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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; }
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -18,7 +19,7 @@ namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("servers")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -61,6 +62,250 @@ namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
</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">
|
||||
<ExcludeAssets>content;contentfiles</ExcludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
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;
|
||||
@@ -29,6 +30,10 @@ public class Startup : MoonlightPlugin
|
||||
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();
|
||||
|
||||
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}");
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/servers/nodes" @attributes="context">
|
||||
<a href="/admin/servers?tab=nodes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="nodeName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@@ -98,7 +98,7 @@
|
||||
$"Successfully created node {Request.Name}"
|
||||
);
|
||||
|
||||
Navigation.NavigateTo("/admin/servers/nodes");
|
||||
Navigation.NavigateTo("/admin/servers?tab=nodes");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/servers/nodes" @attributes="context">
|
||||
<a href="/admin/servers?tab=nodes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="nodeName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@@ -148,7 +148,7 @@
|
||||
$"Successfully updated node {Request.Name}"
|
||||
);
|
||||
|
||||
Navigation.NavigateTo("/admin/servers/nodes");
|
||||
Navigation.NavigateTo("/admin/servers?tab=nodes");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
@page "/admin/servers/nodes"
|
||||
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@@ -15,7 +13,6 @@
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject AlertDialogService AlertDialogService
|
||||
@inject DialogService DialogService
|
||||
@inject ToastService ToastService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IAuthorizationService AuthorizationService
|
||||
@@ -61,7 +58,6 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Title="HTTP Endpoint"
|
||||
Identifier="@nameof(NodeDto.HttpEndpointUrl)"
|
||||
Field="u => u.HttpEndpointUrl"/>
|
||||
<TemplateColumn>
|
||||
<CellTemplate>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,18 @@ public class PermissionProvider : IPermissionProvider
|
||||
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"),
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class SidebarProvider : ISidebarProvider
|
||||
Name = "Servers",
|
||||
IconType = typeof(ServerIcon),
|
||||
Path = "/admin/servers",
|
||||
Policy = Permissions.Nodes.View
|
||||
Policy = Permissions.Servers.View
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moonlight.Frontend" Version="2.1.0"/>
|
||||
<PackageReference Include="ShadcnBlazor" Version="1.0.14"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
32
MoonlightServers.Shared/Admin/Templates/UpdateTemplateDto.cs
Normal file
32
MoonlightServers.Shared/Admin/Templates/UpdateTemplateDto.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MoonlightServers.Shared.Admin.Templates;
|
||||
|
||||
public class UpdateTemplateDto
|
||||
{
|
||||
[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; }
|
||||
|
||||
public UpdateFilesConfigDto FilesConfig { get; set; } = new();
|
||||
public UpdateLifecycleConfigDto LifecycleConfig { get; set; } = new();
|
||||
public UpdateInstallationConfigDto InstallationConfig { get; set; } = new();
|
||||
public UpdateMiscellaneousConfigDto MiscellaneousConfig { get; set; } = new();
|
||||
|
||||
public bool AllowUserDockerImageChange { get; set; }
|
||||
public int? DefaultDockerImageId { get; set; }
|
||||
}
|
||||
18
MoonlightServers.Shared/Admin/Templates/UpdateVariableDto.cs
Normal file
18
MoonlightServers.Shared/Admin/Templates/UpdateVariableDto.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MoonlightServers.Shared.Admin.Templates;
|
||||
|
||||
public class UpdateVariableDto
|
||||
{
|
||||
[Required, MaxLength(30)]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
[Required, MaxLength(255)]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Required, MaxLength(60)]
|
||||
public string EnvName { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? DefaultValue { get; set; } = string.Empty;
|
||||
}
|
||||
9
MoonlightServers.Shared/Admin/Templates/VariableDto.cs
Normal file
9
MoonlightServers.Shared/Admin/Templates/VariableDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MoonlightServers.Shared.Admin.Templates;
|
||||
|
||||
public record VariableDto(
|
||||
int Id,
|
||||
string DisplayName,
|
||||
string Description,
|
||||
string EnvName,
|
||||
string DefaultValue
|
||||
);
|
||||
@@ -14,4 +14,31 @@
|
||||
<Folder Include="Client\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Admin\Templates\UpdateFilesConfigDto.cs">
|
||||
<DependentUpon>UpdateTemplateDto.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Admin\Templates\UpdateLifecycleConfigDto.cs">
|
||||
<DependentUpon>UpdateTemplateDto.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Admin\Templates\UpdateMiscellaneousConfigDto.cs">
|
||||
<DependentUpon>UpdateTemplateDto.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Admin\Templates\UpdateInstallationConfigDto.cs">
|
||||
<DependentUpon>UpdateTemplateDto.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Admin\Templates\FilesConfigDto.cs">
|
||||
<DependentUpon>DetailedTemplateDto.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Admin\Templates\InstallationConfigDto.cs">
|
||||
<DependentUpon>DetailedTemplateDto.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Admin\Templates\LifecycleConfigDto.cs">
|
||||
<DependentUpon>DetailedTemplateDto.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Admin\Templates\MiscellaneousConfigDto.cs">
|
||||
<DependentUpon>DetailedTemplateDto.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,7 +2,8 @@ namespace MoonlightServers.Shared;
|
||||
|
||||
public static class Permissions
|
||||
{
|
||||
public const string Prefix = "Permissions:Servers.";
|
||||
public const string Prefix = "Permissions:MoonlightServers.";
|
||||
|
||||
public static class Nodes
|
||||
{
|
||||
private const string Section = "Nodes";
|
||||
@@ -12,4 +13,24 @@ public static class Permissions
|
||||
public const string Create = $"{Prefix}{Section}.{nameof(Create)}";
|
||||
public const string Delete = $"{Prefix}{Section}.{nameof(Delete)}";
|
||||
}
|
||||
|
||||
public static class Servers
|
||||
{
|
||||
private const string Section = "Servers";
|
||||
|
||||
public const string View = $"{Prefix}{Section}.{nameof(View)}";
|
||||
public const string Edit = $"{Prefix}{Section}.{nameof(Edit)}";
|
||||
public const string Create = $"{Prefix}{Section}.{nameof(Create)}";
|
||||
public const string Delete = $"{Prefix}{Section}.{nameof(Delete)}";
|
||||
}
|
||||
|
||||
public static class Templates
|
||||
{
|
||||
private const string Section = "Templates";
|
||||
|
||||
public const string View = $"{Prefix}{Section}.{nameof(View)}";
|
||||
public const string Edit = $"{Prefix}{Section}.{nameof(Edit)}";
|
||||
public const string Create = $"{Prefix}{Section}.{nameof(Create)}";
|
||||
public const string Delete = $"{Prefix}{Section}.{nameof(Delete)}";
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using MoonlightServers.Shared.Admin.Nodes;
|
||||
using MoonlightServers.Shared.Admin.Templates;
|
||||
|
||||
namespace MoonlightServers.Shared;
|
||||
|
||||
@@ -13,6 +14,23 @@ namespace MoonlightServers.Shared;
|
||||
[JsonSerializable(typeof(NodeDto))]
|
||||
[JsonSerializable(typeof(PagedData<NodeDto>))]
|
||||
|
||||
// - Templates
|
||||
[JsonSerializable(typeof(CreateTemplateDto))]
|
||||
[JsonSerializable(typeof(UpdateTemplateDto))]
|
||||
[JsonSerializable(typeof(TemplateDto))]
|
||||
[JsonSerializable(typeof(DetailedTemplateDto))]
|
||||
[JsonSerializable(typeof(PagedData<TemplateDto>))]
|
||||
|
||||
[JsonSerializable(typeof(VariableDto))]
|
||||
[JsonSerializable(typeof(PagedData<VariableDto>))]
|
||||
[JsonSerializable(typeof(CreateVariableDto))]
|
||||
[JsonSerializable(typeof(UpdateVariableDto))]
|
||||
|
||||
[JsonSerializable(typeof(DockerImageDto))]
|
||||
[JsonSerializable(typeof(PagedData<DockerImageDto>))]
|
||||
[JsonSerializable(typeof(CreateDockerImageDto))]
|
||||
[JsonSerializable(typeof(UpdateDockerImageDto))]
|
||||
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
|
||||
Reference in New Issue
Block a user