15 Commits

Author SHA1 Message Date
2a2ce28b5f Refactored imports to use the latest moonlight core structure 2026-03-13 09:21:35 +01:00
91887ec047 Renamed migration table. Added nullability to template variable migration 2026-03-12 15:45:51 +00:00
2fc371c219 Fixed typo and wrong type reference in DataContext 2026-03-12 15:01:19 +00:00
9470e06c0f Changed migration history table to specific schema. Added log for applied migrations 2026-03-12 14:43:04 +00:00
2f8665f1d4 Added explicit migration assembly marker for DataContext 2026-03-12 14:11:46 +00:00
4d4f35e2be Fixed default destination variable assignment for style build 2026-03-12 13:59:39 +00:00
609ea3a443 Added nuget package settings. Fixed style building
All checks were successful
Dev Publish: Nuget / Publish MoonlightServers.Api (push) Successful in 17s
Dev Publish: Nuget / Publish MoonlightServers.DaemonShared (push) Successful in 11s
Dev Publish: Nuget / Publish MoonlightServers.Frontend (push) Successful in 32s
Dev Publish: Nuget / Publish MoonlightServers.Shared (push) Successful in 11s
2026-03-12 13:53:30 +00:00
3e19b29cde Removed no restore for frontend host project as it would not be restored by default 2026-03-12 13:35:52 +00:00
1475b89660 Updated workflow to handle sources aithout persisting them 2026-03-12 13:31:49 +00:00
3bb9a08630 Added missing moonlight nuget source 2026-03-12 13:20:49 +00:00
252c4103f3 Added workflow for building nuget packages 2026-03-12 13:16:50 +00:00
e7b1e77d0a Implemented template crud, db entities, import/export, ptero and pelican import 2026-03-12 13:00:32 +00:00
7c5dc657dc Implemented node crud and status health check. Added daemon status health endpoint. Refactored project structure. Added sidebar items and ui views 2026-03-05 10:56:52 +00:00
2d1b48b0d4 Implemented statistics. Refactored storage abstractions. Added config options for docker and local storage. Added server service and server updating. 2026-03-02 15:51:05 +00:00
52dbd13fb5 Recreated plugin with new project template. Started implementing server system daemon 2026-03-01 21:09:29 +01:00
435 changed files with 9374 additions and 21490 deletions

View File

@@ -0,0 +1,69 @@
name: "Dev Publish: Nuget"
on:
workflow_dispatch:
push:
branches:
- v2.1
paths:
- 'MoonlightServers.*/*.csproj'
env:
NUGET_SOURCE: https://git.battlestati.one/api/packages/Moonlight-Panel/nuget/index.json
NUGET_PUBLIC: https://api.nuget.org/v3/index.json
CONFIGURATION: Debug
jobs:
publish:
name: Publish ${{ matrix.project }}
runs-on: linux_amd64
strategy:
matrix:
project:
- MoonlightServers.Api
- MoonlightServers.Shared
- MoonlightServers.DaemonShared
- MoonlightServers.Frontend
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Restore NuGet packages
run: >
dotnet restore ${{ matrix.project }}
--source ${{ env.NUGET_PUBLIC }}
--source ${{ env.NUGET_SOURCE }}
# Frontend requires a host build + Tailwind compilation first
- name: Build frontend host (Frontend only)
if: matrix.project == 'MoonlightServers.Frontend'
run: >
dotnet build Hosts/MoonlightServers.Frontend.Host
--configuration ${{ env.CONFIGURATION }}
- name: Build Tailwind styles (Frontend only)
if: matrix.project == 'MoonlightServers.Frontend'
working-directory: Hosts/MoonlightServers.Frontend.Host/Styles
run: npm install && npm run build
- name: Build project
run: >
dotnet build ${{ matrix.project }}
--configuration ${{ env.CONFIGURATION }}
--no-restore
- name: Pack NuGet package
run: >
dotnet pack ${{ matrix.project }}
--configuration ${{ env.CONFIGURATION }}
--output ./artifacts
--no-build
- name: Push NuGet package
run: >
dotnet nuget push ./artifacts/*.nupkg
--skip-duplicate
--source ${{ env.NUGET_SOURCE }}
--api-key ${{ secrets.ACCESS_TOKEN }}

49
.gitignore vendored
View File

@@ -395,44 +395,15 @@ FodyWeavers.xsd
*.msp *.msp
# JetBrains Rider # JetBrains Rider
*.sln.iml **/.idea/**
# User-specific stuff # Style builds
.idea/**/workspace.xml **/style.min.css
.idea/**/tasks.xml **/package-lock.json
.idea/**/usage.statistics.xml **/bun.lock
.idea/**/dictionaries
.idea/**/shelf
# Generated files # Secrets
.idea/**/contentModel.xml **/.env
**/appsettings.json
# Sensitive or high-churn files **/appsettings.Development.json
.idea/**/dataSources/ **/storage
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Moonlight
storage/
.idea/**/.idea
MoonlightServers.min.css
core.min.css
# Build script for nuget packages
finalPackages/
nupkgs/
# Local daemon tests
**/data/volumes/**
# Local plugin build
tmp/
style.min.css

View File

@@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<!-- Put your plugin references here -->
<!-- E.g. <PackageReference Include="MoonlightServers.Api" Version="2.1.0" /> -->
</ItemGroup>
</Project>

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.5"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\MoonlightServers.Api\MoonlightServers.Api.csproj" />
<ProjectReference Include="..\MoonlightServers.Frontend.Host\MoonlightServers.Frontend.Host.csproj" />
</ItemGroup>
<Import Project="Api.props"/>
</Project>

View File

@@ -0,0 +1,9 @@
using Moonlight.Api;
using SimplePlugin.Generated;
var plugins = PluginRegistry
.Modules
.OfType<MoonlightPlugin>()
.ToArray();
await StartupHandler.RunAsync(args, plugins);

View File

@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5031",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Moonlight": {
"Database": {
"Host": "your-db.host",
"Username": "change_me",
"Password": "change_me",
"Database": "change_me"
},
"Oidc": {
"Authority": "http://localhost:8092",
"RequireHttpsMetadata": false,
"ClientId": "clientId",
"ClientSecret": "clientSecret"
}
}
}

View File

@@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<!-- Put your plugin references here -->
<!-- E.g. <PackageReference Include="MoonlightServers.Frontend" Version="2.1.0" /> -->
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" PrivateAssets="all"/>
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\MoonlightServers.Frontend\MoonlightServers.Frontend.csproj" />
</ItemGroup>
<Import Project="Frontend.props"/>
</Project>

View File

@@ -0,0 +1,9 @@
using Moonlight.Frontend;
using SimplePlugin.Generated;
var plugins = PluginRegistry
.Modules
.OfType<MoonlightPlugin>()
.ToArray();
await StartupHandler.RunAsync(args, plugins);

View File

@@ -0,0 +1,25 @@
import fs from 'fs';
import selectorParser from 'postcss-selector-parser';
export default function extractTailwindClasses(opts = {}) {
const classSet = new Set();
return {
postcssPlugin: 'extract-tailwind-classes',
Rule(rule) {
selectorParser(selectors => {
selectors.walkClasses(node => {
classSet.add(node.value);
});
}).processSync(rule.selector);
},
OnceExit() {
const classArray = Array.from(classSet).sort();
fs.mkdirSync('../../../MoonlightServers.Frontend/Styles', { recursive: true });
fs.writeFileSync('../../../MoonlightServers.Frontend/Styles/MoonlightServers.Frontend.map', classArray.join('\n'));
console.log(`Extracted classes ${classArray.length}`);
}
};
}
extractTailwindClasses.postcss = true;

View File

@@ -0,0 +1,19 @@
{
"scripts": {
"dev": "npx postcss styles.css -o ../wwwroot/style.min.css --watch",
"dev-build": "npx postcss styles.css -o ../wwwroot/style.min.css",
"build": "cross-env EXTRACT_CLASSES=true npx postcss styles.css -o ../wwwroot/style.min.css"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.18",
"cssnano": "^7.1.2",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"postcss-selector-parser": "^7.1.1",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"cross-env": "^10.1.0"
}
}

View File

@@ -0,0 +1,18 @@
import tailwindcss from '@tailwindcss/postcss';
import cssnano from 'cssnano';
import extractTailwindClasses from './extract-classes.mjs';
const plugins = [
tailwindcss,
cssnano({
preset: 'default'
})
];
if (process.env.EXTRACT_CLASSES === "true") {
plugins.push(extractTailwindClasses());
}
export default {
plugins
};

View File

@@ -0,0 +1,31 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "../bin/ShadcnBlazor/scrollbar.css";
@import "../bin/ShadcnBlazor/default-theme.css";
@import "./theme.css";
@source "../bin/**/*.map";
@source "../../../MoonlightServers.Api/**/*.razor";
@source "../../../MoonlightServers.Api/**/*.cs";
@source "../../../MoonlightServers.Api/**/*.html";
@source "../../../MoonlightServers.Frontend/**/*.razor";
@source "../../../MoonlightServers.Frontend/**/*.cs";
@source "../../../MoonlightServers.Frontend/**/*.html";
@custom-variant dark (&:is(.dark *));
#blazor-error-ui {
display: none;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,132 @@
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
--font-sans: Inter, sans-serif;
--font-serif: Georgia, serif;
}
.dark {
/* Deep blue-slate background with purple undertones */
--background: oklch(0.16 0.028 260);
--foreground: oklch(0.98 0.008 260);
/* Cards with slightly lighter blue-slate */
--card: oklch(0.21 0.032 260);
--card-foreground: oklch(0.98 0.008 260);
/* Popovers with medium depth */
--popover: oklch(0.24 0.035 260);
--popover-foreground: oklch(0.98 0.008 260);
/* Vibrant blue-purple primary */
--primary: oklch(0.62 0.18 270);
--primary-foreground: oklch(0.99 0.005 260);
/* Secondary with blue-slate tone */
--secondary: oklch(0.27 0.038 260);
--secondary-foreground: oklch(0.98 0.008 260);
/* Muted elements */
--muted: oklch(0.25 0.035 260);
--muted-foreground: oklch(0.66 0.025 260);
/* Accent with purple-blue blend */
--accent: oklch(0.36 0.065 268);
--accent-foreground: oklch(0.98 0.008 260);
/* Destructive red with good contrast */
--destructive: oklch(0.62 0.22 25);
--destructive-foreground: oklch(0.99 0.005 260);
/* Subtle borders and inputs */
--border: oklch(0.32 0.025 260);
--input: oklch(0.30 0.030 260);
--ring: oklch(0.62 0.18 270);
/* Chart colors with blue-purple harmony */
--chart-1: oklch(0.58 0.18 270);
--chart-2: oklch(0.62 0.16 245);
--chart-3: oklch(0.68 0.15 290);
--chart-4: oklch(0.60 0.20 260);
--chart-5: oklch(0.65 0.14 280);
/* Sidebar with slightly different depth */
--sidebar: oklch(0.18 0.030 260);
--sidebar-foreground: oklch(0.97 0.008 260);
--sidebar-primary: oklch(0.60 0.17 270);
--sidebar-primary-foreground: oklch(0.99 0.005 260);
--sidebar-accent: oklch(0.26 0.038 260);
--sidebar-accent-foreground: oklch(0.98 0.008 260);
--sidebar-border: oklch(0.30 0.025 260);
--sidebar-ring: oklch(0.58 0.17 270);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moonlight</title>
<base href="/" />
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback" />
<link rel="stylesheet" href="style.min.css" />
<script type="importmap"></script>
<script>
window.frontendConfig = {
STYLE_TAG_ID: 'theme-variables',
configuration: {},
applyTheme: function(cssContent) {
// Find or create the style tag
let styleTag = document.getElementById(this.STYLE_TAG_ID);
if (!styleTag) {
styleTag = document.createElement('style');
styleTag.id = this.STYLE_TAG_ID;
document.head.appendChild(styleTag);
}
// Update the style tag content
styleTag.textContent = cssContent;
},
reloadConfiguration: function (){
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/frontend/config', false);
xhr.send(null);
if (xhr.status === 200) {
this.configuration = JSON.parse(xhr.responseText);
}
} catch (error) {
console.error('Failed to load initial theme:', error);
}
},
getConfiguration: function (){
return this.configuration;
},
reload: function () {
this.reloadConfiguration();
document.title = this.configuration.name;
this.applyTheme(this.configuration.themeCss);
}
};
window.frontendConfig.reload();
</script>
</head>
<body class="bg-background text-foreground">
<div id="app">
<div class="h-screen w-full flex items-center justify-center">
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-3 rounded-lg border-dashed p-6 text-center text-balance md:p-12">
<div class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground size-10 rounded-lg [&_svg:not([class*='size-'])]:size-6">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-zap-icon lucide-zap size-6">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg>
</div>
</div>
<div class="text-lg font-medium tracking-tight">
Loading application
</div>
<div class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="bg-primary/20 w-full relative h-2 overflow-hidden rounded-full">
<div class="bg-primary h-full w-[var(--blazor-load-percentage,0)] flex-1 transition-all">
</div>
</div>
</div>
</div>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="/_content/ShadcnBlazor/interop.js" defer></script>
<script src="/_content/ShadcnBlazor.Extras/interop.js" defer></script>
<script src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js" defer></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>

View File

@@ -0,0 +1,157 @@
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Shared.Shared;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
using MoonlightServers.Shared;
using MoonlightServers.Shared.Admin.Nodes;
namespace MoonlightServers.Api.Admin.Nodes;
[ApiController]
[Route("api/admin/servers/nodes")]
public class CrudController : Controller
{
private readonly DatabaseRepository<Node> DatabaseRepository;
private readonly HybridCache Cache;
public CrudController(
DatabaseRepository<Node> databaseRepository,
HybridCache cache
)
{
DatabaseRepository = databaseRepository;
Cache = cache;
}
[HttpGet]
[Authorize(Policy = Permissions.Nodes.View)]
public async Task<ActionResult<PagedData<NodeDto>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int length,
[FromQuery] FilterOptions? filterOptions
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
// Query building
var query = DatabaseRepository
.Query();
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Node.Name) =>
query.Where(role => EF.Functions.ILike(role.Name, $"%{filterOption.Value}%")),
_ => query
};
}
}
// Pagination
var data = await query
.OrderBy(x => x.Id)
.ProjectToDto()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<NodeDto>(data, total);
}
[HttpGet("{id:int}")]
[Authorize(Policy = Permissions.Nodes.View)]
public async Task<ActionResult<NodeDto>> GetAsync([FromRoute] int id)
{
var node = await DatabaseRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
return NodeMapper.ToDto(node);
}
[HttpPost]
[Authorize(Policy = Permissions.Nodes.Create)]
public async Task<ActionResult<NodeDto>> CreateAsync([FromBody] CreateNodeDto request)
{
var node = NodeMapper.ToEntity(request);
node.TokenId = GenerateString(10);
node.Token = GenerateString(64);
var finalRole = await DatabaseRepository.AddAsync(node);
return NodeMapper.ToDto(finalRole);
}
[HttpPut("{id:int}")]
[Authorize(Policy = Permissions.Nodes.Edit)]
public async Task<ActionResult<NodeDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateNodeDto request)
{
var node = await DatabaseRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
NodeMapper.Merge(node, request);
await DatabaseRepository.UpdateAsync(node);
return NodeMapper.ToDto(node);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = Permissions.Nodes.Delete)]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var node = await DatabaseRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
await DatabaseRepository.RemoveAsync(node);
// Remove cache for node token auth scheme
await Cache.RemoveAsync(string.Format(NodeTokenSchemeHandler.CacheKeyFormat, node.TokenId));
return NoContent();
}
private static string GenerateString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var stringBuilder = new StringBuilder();
var random = new Random();
for (var i = 0; i < length; i++)
{
stringBuilder.Append(chars[random.Next(chars.Length)]);
}
return stringBuilder.ToString();
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Shared;
using MoonlightServers.Shared.Admin.Nodes;
namespace MoonlightServers.Api.Admin.Nodes;
[ApiController]
[Authorize(Policy = Permissions.Nodes.View)]
[Route("api/admin/servers/nodes/{id:int}/health")]
public class HealthController : Controller
{
private readonly DatabaseRepository<Node> DatabaseRepository;
private readonly NodeService NodeService;
private readonly ILogger<HealthController> Logger;
public HealthController(
DatabaseRepository<Node> databaseRepository,
NodeService nodeService,
ILogger<HealthController> logger
)
{
DatabaseRepository = databaseRepository;
NodeService = nodeService;
Logger = logger;
}
[HttpGet]
public async Task<ActionResult<NodeHealthDto>> GetAsync([FromRoute] int id)
{
var node = await DatabaseRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
var health = await NodeService.GetHealthAsync(node);
return new NodeHealthDto()
{
StatusCode = health.StatusCode,
RemoteStatusCode = health.Dto?.RemoteStatusCode ?? 0,
IsHealthy = health is { StatusCode: >= 200 and <= 299, Dto.RemoteStatusCode: >= 200 and <= 299 }
};
}
}

View File

@@ -0,0 +1,17 @@
using System.Diagnostics.CodeAnalysis;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Shared.Admin.Nodes;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.Api.Admin.Nodes;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class NodeMapper
{
public static partial NodeDto ToDto(Node node);
public static partial IQueryable<NodeDto> ProjectToDto(this IQueryable<Node> nodes);
public static partial Node ToEntity(CreateNodeDto dto);
public static partial void Merge([MappingTarget] Node node, UpdateNodeDto dto);
}

View File

@@ -0,0 +1,132 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.DaemonShared.Http;
using MoonlightServers.DaemonShared.Http.Daemon;
namespace MoonlightServers.Api.Admin.Nodes;
public class NodeService
{
private readonly IHttpClientFactory ClientFactory;
private readonly ILogger<NodeService> Logger;
public NodeService(IHttpClientFactory clientFactory, ILogger<NodeService> logger)
{
ClientFactory = clientFactory;
Logger = logger;
}
public async Task<NodeHealthStatus> GetHealthAsync(Node node)
{
var client = ClientFactory.CreateClient();
var request = CreateBaseRequest(node, HttpMethod.Get, "api/health");
try
{
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
return new NodeHealthStatus((int)response.StatusCode, null);
try
{
var health = await response
.Content
.ReadFromJsonAsync<HealthDto>(SerializationContext.Default.Options);
return new NodeHealthStatus((int)response.StatusCode, health);
}
catch (Exception e)
{
Logger.LogTrace(e, "An unhandled error occured while processing health response of node {id}", node.Id);
return new NodeHealthStatus((int)response.StatusCode, null);
}
}
catch (Exception e)
{
Logger.LogTrace(e, "An error occured while fetching health status of node {id}", node.Id);
return new NodeHealthStatus(0, null);
}
}
private static HttpRequestMessage CreateBaseRequest(
Node node,
[StringSyntax(StringSyntaxAttribute.Uri)]
HttpMethod method,
string endpoint
)
{
var request = new HttpRequestMessage();
request.Headers.Add(HeaderNames.Authorization, node.Token);
request.RequestUri = new Uri(new Uri(node.HttpEndpointUrl), endpoint);
request.Method = method;
return request;
}
private async Task EnsureSuccessAsync(HttpResponseMessage message)
{
if (message.IsSuccessStatusCode)
return;
try
{
var problemDetails = await message.Content.ReadFromJsonAsync<ProblemDetails>(
SerializationContext.Default.Options
);
if (problemDetails == null)
{
// If we cant handle problem details, we handle it natively
message.EnsureSuccessStatusCode();
return;
}
// Parse into exception
throw new NodeException(
problemDetails.Type,
problemDetails.Title,
problemDetails.Status,
problemDetails.Detail,
problemDetails.Errors
);
}
catch (JsonException)
{
// If we cant handle problem details, we handle it natively
message.EnsureSuccessStatusCode();
}
}
}
public record NodeHealthStatus(int StatusCode, HealthDto? Dto);
public class NodeException : Exception
{
public string Type { get; }
public string Title { get; }
public int Status { get; }
public string? Detail { get; }
public Dictionary<string, string[]>? Errors { get; }
public NodeException(
string type,
string title,
int status,
string? detail = null,
Dictionary<string, string[]>? errors = null)
: base(detail ?? title)
{
Type = type;
Title = title;
Status = status;
Detail = detail;
Errors = errors;
}
}

View File

@@ -0,0 +1,172 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Shared.Shared;
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();
}
}

View File

@@ -0,0 +1,144 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Shared.Shared;
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();
}
}

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

View File

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

View 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);
}

View File

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

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

View 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);
}
}
}

View File

@@ -0,0 +1,144 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Shared.Shared;
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();
}
}

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Api.Infrastructure.Configuration;
public class NodeTokenOptions
{
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
}

View File

@@ -0,0 +1,66 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Moonlight.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
namespace MoonlightServers.Api.Infrastructure.Database;
public class DataContext : DbContext
{
public DbSet<Node> Nodes { get; set; }
public DbSet<Template> Templates { get; set; }
public DbSet<TemplateDockerImage> TemplateDockerImages { get; set; }
public DbSet<TemplateVariable> TemplateVariablesVariables { get; set; }
private readonly IOptions<DatabaseOptions> Options;
public DataContext(IOptions<DatabaseOptions> options)
{
Options = options;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
return;
optionsBuilder.UseNpgsql(
$"Host={Options.Value.Host};" +
$"Port={Options.Value.Port};" +
$"Username={Options.Value.Username};" +
$"Password={Options.Value.Password};" +
$"Database={Options.Value.Database}",
builder =>
{
builder.MigrationsAssembly(typeof(DataContext).Assembly);
builder.MigrationsHistoryTable("MigrationsHistory", "servers");
}
);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("servers");
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Template>()
.ComplexProperty(x => x.FilesConfig, builder => builder.ToJson())
.ComplexProperty(x => x.LifecycleConfig, builder => builder.ToJson())
.ComplexProperty(x => x.InstallationConfig, builder => builder.ToJson())
.ComplexProperty(x => x.MiscellaneousConfig, builder => builder.ToJson());
// One-to-many: Template => DockerImages
modelBuilder.Entity<Template>()
.HasMany(t => t.DockerImages)
.WithOne(d => d.Template)
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade);
// One-to-one: Template => DefaultDockerImage
modelBuilder.Entity<Template>()
.HasOne(t => t.DefaultDockerImage)
.WithOne()
.HasForeignKey<Template>("DefaultDockerImageId")
.OnDelete(DeleteBehavior.SetNull);
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore;
using MoonlightServers.Api.Infrastructure.Database.Interfaces;
namespace MoonlightServers.Api.Infrastructure.Database;
public class DatabaseRepository<T> where T : class
{
private readonly DataContext DataContext;
private readonly DbSet<T> Set;
public DatabaseRepository(DataContext dataContext)
{
DataContext = dataContext;
Set = DataContext.Set<T>();
}
public IQueryable<T> Query() => Set;
public async Task<T> AddAsync(T entity)
{
if (entity is IActionTimestamps actionTimestamps)
{
actionTimestamps.CreatedAt = DateTimeOffset.UtcNow;
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
}
var final = Set.Add(entity);
await DataContext.SaveChangesAsync();
return final.Entity;
}
public async Task UpdateAsync(T entity)
{
if (entity is IActionTimestamps actionTimestamps)
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
Set.Update(entity);
await DataContext.SaveChangesAsync();
}
public async Task RemoveAsync(T entity)
{
Set.Remove(entity);
await DataContext.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MoonlightServers.Api.Infrastructure.Database;
public class DbMigrationService : IHostedLifecycleService
{
private readonly ILogger<DbMigrationService> Logger;
private readonly IServiceProvider ServiceProvider;
public DbMigrationService(ILogger<DbMigrationService> logger, IServiceProvider serviceProvider)
{
Logger = logger;
ServiceProvider = serviceProvider;
}
public async Task StartingAsync(CancellationToken cancellationToken)
{
Logger.LogTrace("Checking for pending migrations");
await using var scope = ServiceProvider.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<DataContext>();
var availableMigrations = context.Database.GetMigrations();
Logger.LogTrace("Available migrations: {names}", string.Join(", ", availableMigrations));
var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken);
var migrationNames = pendingMigrations.ToArray();
if (migrationNames.Length == 0)
{
Logger.LogTrace("No pending migrations found");
return;
}
Logger.LogInformation("Pending migrations: {names}", string.Join(", ", migrationNames));
Logger.LogInformation("Migration started");
await context.Database.MigrateAsync(cancellationToken);
Logger.LogInformation("Migration complete");
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using MoonlightServers.Api.Infrastructure.Database.Interfaces;
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
public class Node : IActionTimestamps
{
public int Id { get; set; }
[MaxLength(50)]
public string Name { get; set; }
[MaxLength(100)]
public string HttpEndpointUrl { get; set; }
[MaxLength(10)]
public string TokenId { get; set; }
[MaxLength(64)]
public string Token { get; set; }
// Action timestamps
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Api.Infrastructure.Database.Interfaces;
internal interface IActionTimestamps
{
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Api.Infrastructure.Database.Json;
public class MiscellaneousConfig
{
public bool UseLegacyStartup { get; set; }
}

View File

@@ -0,0 +1,70 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20260305104238_AddedBasicNodeEntity")]
partial class AddedBasicNodeEntity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("HttpEndpointUrl")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Nodes", "servers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
{
/// <inheritdoc />
public partial class AddedBasicNodeEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "servers");
migrationBuilder.CreateTable(
name: "Nodes",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
HttpEndpointUrl = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
TokenId = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
Token = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Nodes", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Nodes",
schema: "servers");
}
}
}

View 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
}
}
}

View File

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

View File

@@ -0,0 +1,313 @@
// <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("20260312153948_AddedNullabilityForTemplateVariableDefaultValue")]
partial class AddedNullabilityForTemplateVariableDefaultValue
{
/// <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");
});
});
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")
.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
}
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
{
/// <inheritdoc />
public partial class AddedNullabilityForTemplateVariableDefaultValue : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "DefaultValue",
schema: "servers",
table: "TemplateVariablesVariables",
type: "character varying(1024)",
maxLength: 1024,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(1024)",
oldMaxLength: 1024);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "DefaultValue",
schema: "servers",
table: "TemplateVariablesVariables",
type: "character varying(1024)",
maxLength: 1024,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "character varying(1024)",
oldMaxLength: 1024,
oldNullable: true);
}
}
}

View File

@@ -0,0 +1,310 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
{
[DbContext(typeof(DataContext))]
partial class DataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("HttpEndpointUrl")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Nodes", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowUserDockerImageChange")
.HasColumnType("boolean");
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int?>("DefaultDockerImageId")
.HasColumnType("integer");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("DonateUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("UpdateUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
{
b1.IsRequired();
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
{
b2.IsRequired();
b2.Property<string>("Parser")
.IsRequired();
b2.Property<string>("Path")
.IsRequired();
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
{
b3.IsRequired();
b3.Property<string>("Key")
.IsRequired();
b3.Property<string>("Value");
});
});
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")
.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
}
}
}

View File

@@ -0,0 +1,101 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
namespace MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
public class NodeTokenSchemeHandler : AuthenticationHandler<NodeTokenSchemeOptions>
{
public const string SchemeName = "MoonlightServers.NodeToken";
public const string CacheKeyFormat = $"MoonlightServers.{nameof(NodeTokenSchemeHandler)}.{{0}}";
private readonly DatabaseRepository<Node> DatabaseRepository;
private readonly HybridCache Cache;
public NodeTokenSchemeHandler(
IOptionsMonitor<NodeTokenSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
DatabaseRepository<Node> databaseRepository,
HybridCache cache
) : base(options, logger, encoder)
{
DatabaseRepository = databaseRepository;
Cache = cache;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Basic format validation
if (!Context.Request.Headers.TryGetValue(HeaderNames.Authorization, out var authHeaderValues))
return AuthenticateResult.Fail("No authorization header present");
if (authHeaderValues.Count != 1)
return AuthenticateResult.Fail("No authorization value present");
var authHeaderValue = authHeaderValues[0];
if (string.IsNullOrEmpty(authHeaderValue))
return AuthenticateResult.Fail("No authorization value present");
var authHeaderParts = authHeaderValue.Split(
' ',
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries
);
// Validate parts
if (authHeaderParts.Length < 2)
return AuthenticateResult.Fail("Malformed authorization header");
var tokenId = authHeaderParts[0];
var token = authHeaderParts[1];
if (tokenId.Length != 10 && token.Length != 64)
return AuthenticateResult.Fail("Malformed authorization header");
// Real validation
var cacheKey = string.Format(CacheKeyFormat, tokenId);
var session = await Cache.GetOrCreateAsync<NodeTokenSession?>(cacheKey, async cancellationToken =>
{
return await DatabaseRepository
.Query()
.Where(x => x.TokenId == tokenId)
.Select(x => new NodeTokenSession(x.Id, x.Token))
.FirstOrDefaultAsync(cancellationToken: cancellationToken);
},
new HybridCacheEntryOptions()
{
LocalCacheExpiration = Options.LookupCacheL1Expiry,
Expiration = Options.LookupCacheL2Expiry
}
);
if(session == null || token != session.Token)
return AuthenticateResult.Fail("Invalid authorization header");
// All checks have passed, create auth ticket
return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(
new ClaimsIdentity(
[
new Claim("NodeId", session.Id.ToString())
],
SchemeName
)
),
SchemeName
));
}
private record NodeTokenSession(int Id, string Token);
}

View File

@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Authentication;
namespace MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
public class NodeTokenSchemeOptions : AuthenticationSchemeOptions
{
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
}

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup Label="Nuget Package Settings">
<Version>2.1.0</Version>
<Title>MoonlightServers.Api</Title>
<Authors>Moonlight Panel</Authors>
<Description>Development package of MoonlightServers.Api</Description>
<Copyright>Moonlight Panel</Copyright>
<PackageProjectUrl>https://git.battlestati.one/Moonlight-Panel/Servers</PackageProjectUrl>
<PackageLicenseUrl>https://git.battlestati.one/Moonlight-Panel/Servers/src/branch/v2.1/LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://git.battlestati.one/Moonlight-Panel/Servers</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moonlight.Api" Version="2.1.0">
<ExcludeAssets>content;contentfiles</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Client\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
<ProjectReference Include="..\MoonlightServers.Shared\MoonlightServers.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
namespace MoonlightServers.Api.Remote.Nodes;
[ApiController]
[Route("api/remote/servers/nodes/ping")]
[Authorize(AuthenticationSchemes = NodeTokenSchemeHandler.SchemeName)]
public class PingController : Controller
{
[HttpGet]
public ActionResult Get() => NoContent();
}

View File

@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
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;
using MoonlightServers.Shared;
using SimplePlugin.Abstractions;
namespace MoonlightServers.Api;
[PluginModule]
public class Startup : MoonlightPlugin
{
public override void PreBuild(WebApplicationBuilder builder)
{
builder.Services.AddControllers()
.AddApplicationPart(typeof(Startup).Assembly)
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
});
builder.Services.AddScoped(typeof(DatabaseRepository<>));
builder.Services.AddDbContext<DataContext>();
builder.Services.AddHostedService<DbMigrationService>();
builder.Services.AddScoped<TemplateTransferService>();
builder.Services.AddScoped<PterodactylEggImportService>();
builder.Services.AddScoped<PelicanEggImportService>();
builder.Services.AddSingleton<NodeService>();
var nodeTokenOptions = new NodeTokenOptions();
builder.Configuration.Bind("Moonlight:Servers:NodeToken", nodeTokenOptions);
builder.Services
.AddAuthentication()
.AddScheme<NodeTokenSchemeOptions, NodeTokenSchemeHandler>(NodeTokenSchemeHandler.SchemeName, options =>
{
options.LookupCacheL1Expiry = nodeTokenOptions.LookupCacheL1Expiry;
options.LookupCacheL2Expiry = nodeTokenOptions.LookupCacheL2Expiry;
});
builder.Logging.AddFilter(
"MoonlightServers.Api.Infrastructure.Implementations.NodeToken.NodeTokenSchemeHandler",
LogLevel.Warning
);
}
}

View File

@@ -1,10 +0,0 @@
using MoonCore.PluginFramework;
using Moonlight.ApiServer.Plugins;
namespace MoonlightServers.ApiServer.Runtime;
[PluginLoader]
public partial class DevPluginLoader : IPluginStartup
{
}

View File

@@ -1,31 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MoonlightServers.ApiServer\MoonlightServers.ApiServer.csproj"/>
<ProjectReference Include="..\MoonlightServers.Frontend.Runtime\MoonlightServers.Frontend.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.9" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
</ItemGroup>
<ItemGroup>
<Content Update="Properties\launchSettings.json">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@@ -1,34 +0,0 @@
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Startup;
using MoonlightServers.ApiServer.Runtime;
var pluginLoader = new DevPluginLoader();
pluginLoader.Initialize();
var builder = WebApplication.CreateBuilder(args);
builder.AddMoonlight(pluginLoader.Instances);
var app = builder.Build();
app.UseMoonlight(pluginLoader.Instances);
// Add frontend
var configuration = AppConfiguration.CreateEmpty();
builder.Configuration.Bind(configuration);
// Handle setup of wasm app hosting in the runtime
// so the Moonlight.ApiServer doesn't need the wasm package
if (configuration.Frontend.EnableHosting)
{
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
}
app.MapMoonlight(pluginLoader.Instances);
await app.RunAsync();

View File

@@ -1,18 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Dev Server": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5269",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"MOONLIGHT_PUBLICURL": "http://localhost:5269",
"HTTP_PROXY": "",
"HTTPS_PROXY": ""
}
}
}
}

View File

@@ -1,13 +0,0 @@
namespace MoonlightServers.ApiServer.Database.Entities;
public class Allocation
{
public int Id { get; set; }
// Relations
public Node Node { get; set; }
public Server? Server { get; set; }
public string IpAddress { get; set; }
public int Port { get; set; }
}

View File

@@ -1,21 +0,0 @@
namespace MoonlightServers.ApiServer.Database.Entities;
public class Node
{
public int Id { get; set; }
// Relations
public List<Server> Servers { get; set; } = new();
public List<Allocation> Allocations { get; set; } = new();
// Meta
public string Name { get; set; }
// Connection details
public string Fqdn { get; set; }
public string Token { get; set; }
public string TokenId { get; set; }
public int HttpPort { get; set; }
public int FtpPort { get; set; }
public bool UseSsl { get; set; }
}

View File

@@ -1,27 +0,0 @@
namespace MoonlightServers.ApiServer.Database.Entities;
public class Server
{
public int Id { get; set; }
// Relations
public Star Star { get; set; }
public Node Node { get; set; }
public List<Allocation> Allocations { get; set; } = new();
public List<ServerVariable> Variables { get; set; } = new();
public List<ServerBackup> Backups { get; set; } = new();
public List<ServerShare> Shares { get; set; } = new();
// Meta
public string Name { get; set; }
public int OwnerId { get; set; }
// Star specific stuff
public string? StartupOverride { get; set; }
public int DockerImageIndex { get; set; }
// Resources and limits
public int Cpu { get; set; }
public int Memory { get; set; }
public int Disk { get; set; }
}

View File

@@ -1,13 +0,0 @@
namespace MoonlightServers.ApiServer.Database.Entities;
public class ServerBackup
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime CompletedAt { get; set; }
public long Size { get; set; }
public bool Successful { get; set; }
public bool Completed { get; set; }
}

View File

@@ -1,20 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
using MoonlightServers.ApiServer.Models;
namespace MoonlightServers.ApiServer.Database.Entities;
public class ServerShare
{
public int Id { get; set; }
public int UserId { get; set; }
public Server Server { get; set; }
public ServerShareContent Content { get; set; } = new();
[Column(TypeName="timestamp with time zone")]
public DateTime CreatedAt { get; set; }
[Column(TypeName="timestamp with time zone")]
public DateTime UpdatedAt { get; set; }
}

View File

@@ -1,12 +0,0 @@
namespace MoonlightServers.ApiServer.Database.Entities;
public class ServerVariable
{
public int Id { get; set; }
// Relations
public Server Server { get; set; }
public string Key { get; set; }
public string Value { get; set; }
}

View File

@@ -1,33 +0,0 @@
namespace MoonlightServers.ApiServer.Database.Entities;
public class Star
{
public int Id { get; set; }
// References
public List<StarVariable> Variables { get; set; } = new();
public List<StarDockerImage> DockerImages { get; set; } = new();
// Meta
public string Name { get; set; }
public string Version { get; set; }
public string Author { get; set; }
public string? UpdateUrl { get; set; }
public string? DonateUrl { get; set; }
// Start and stop
public string StartupCommand { get; set; }
public string StopCommand { get; set; }
public string OnlineDetection { get; set; }
// Install
public string InstallShell { get; set; }
public string InstallDockerImage { get; set; }
public string InstallScript { get; set; }
// Misc
public int RequiredAllocations { get; set; }
public bool AllowDockerImageChange { get; set; }
public int DefaultDockerImage { get; set; }
public string ParseConfiguration { get; set; }
}

View File

@@ -1,11 +0,0 @@
namespace MoonlightServers.ApiServer.Database.Entities;
public class StarDockerImage
{
public int Id { get; set; }
public Star Star { get; set; }
public string DisplayName { get; set; }
public string Identifier { get; set; }
public bool AutoPulling { get; set; }
}

View File

@@ -1,21 +0,0 @@
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Database.Entities;
public class StarVariable
{
public int Id { get; set; }
public Star Star { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Key { get; set; }
public string DefaultValue { get; set; }
public bool AllowViewing { get; set; }
public bool AllowEditing { get; set; }
public StarVariableType Type { get; set; }
public string? Filter { get; set; }
}

View File

@@ -1,529 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
[DbContext(typeof(ServersDataContext))]
[Migration("20250922091731_RecreatedModelsInNewSchema")]
partial class RecreatedModelsInNewSchema
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("IpAddress")
.IsRequired()
.HasColumnType("text");
b.Property<int>("NodeId")
.HasColumnType("integer");
b.Property<int>("Port")
.HasColumnType("integer");
b.Property<int?>("ServerId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("ServerId");
b.ToTable("Allocations", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Fqdn")
.IsRequired()
.HasColumnType("text");
b.Property<int>("FtpPort")
.HasColumnType("integer");
b.Property<int>("HttpPort")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TokenId")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("UseSsl")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("Nodes", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Cpu")
.HasColumnType("integer");
b.Property<int>("Disk")
.HasColumnType("integer");
b.Property<int>("DockerImageIndex")
.HasColumnType("integer");
b.Property<int>("Memory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("NodeId")
.HasColumnType("integer");
b.Property<int>("OwnerId")
.HasColumnType("integer");
b.Property<int>("StarId")
.HasColumnType("integer");
b.Property<string>("StartupOverride")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("Completed")
.HasColumnType("boolean");
b.Property<DateTime>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("ServerId")
.HasColumnType("integer");
b.Property<long>("Size")
.HasColumnType("bigint");
b.Property<bool>("Successful")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerBackups", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerShare", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("ServerId")
.HasColumnType("integer");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerShares", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<int>("ServerId")
.HasColumnType("integer");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowDockerImageChange")
.HasColumnType("boolean");
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<int>("DefaultDockerImage")
.HasColumnType("integer");
b.Property<string>("DonateUrl")
.HasColumnType("text");
b.Property<string>("InstallDockerImage")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstallScript")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstallShell")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OnlineDetection")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ParseConfiguration")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RequiredAllocations")
.HasColumnType("integer");
b.Property<string>("StartupCommand")
.IsRequired()
.HasColumnType("text");
b.Property<string>("StopCommand")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UpdateUrl")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Stars", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AutoPulling")
.HasColumnType("boolean");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StarId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StarId");
b.ToTable("StarDockerImages", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowEditing")
.HasColumnType("boolean");
b.Property<bool>("AllowViewing")
.HasColumnType("boolean");
b.Property<string>("DefaultValue")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Filter")
.HasColumnType("text");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StarId")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StarId");
b.ToTable("StarVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node")
.WithMany("Allocations")
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Allocations")
.HasForeignKey("ServerId");
b.Navigation("Node");
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node")
.WithMany("Servers")
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany()
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Node");
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", null)
.WithMany("Backups")
.HasForeignKey("ServerId");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerShare", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Shares")
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("MoonlightServers.ApiServer.Models.ServerShareContent", "Content", b1 =>
{
b1.Property<int>("ServerShareId")
.HasColumnType("integer");
b1.HasKey("ServerShareId");
b1.ToTable("ServerShares", "servers");
b1.ToJson("Content");
b1.WithOwner()
.HasForeignKey("ServerShareId");
b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerShareContent+SharePermission", "Permissions", b2 =>
{
b2.Property<int>("ServerShareContentServerShareId")
.HasColumnType("integer");
b2.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
b2.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b2.Property<int>("Level")
.HasColumnType("integer");
b2.HasKey("ServerShareContentServerShareId", "__synthesizedOrdinal");
b2.ToTable("ServerShares", "servers");
b2.WithOwner()
.HasForeignKey("ServerShareContentServerShareId");
});
b1.Navigation("Permissions");
});
b.Navigation("Content")
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Variables")
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany("DockerImages")
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany("Variables")
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Navigation("Allocations");
b.Navigation("Servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Navigation("Allocations");
b.Navigation("Backups");
b.Navigation("Shares");
b.Navigation("Variables");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,353 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class RecreatedModelsInNewSchema : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "servers");
migrationBuilder.CreateTable(
name: "Nodes",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Fqdn = table.Column<string>(type: "text", nullable: false),
Token = table.Column<string>(type: "text", nullable: false),
TokenId = table.Column<string>(type: "text", nullable: false),
HttpPort = table.Column<int>(type: "integer", nullable: false),
FtpPort = table.Column<int>(type: "integer", nullable: false),
UseSsl = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Nodes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Stars",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Version = table.Column<string>(type: "text", nullable: false),
Author = table.Column<string>(type: "text", nullable: false),
UpdateUrl = table.Column<string>(type: "text", nullable: true),
DonateUrl = table.Column<string>(type: "text", nullable: true),
StartupCommand = table.Column<string>(type: "text", nullable: false),
StopCommand = table.Column<string>(type: "text", nullable: false),
OnlineDetection = table.Column<string>(type: "text", nullable: false),
InstallShell = table.Column<string>(type: "text", nullable: false),
InstallDockerImage = table.Column<string>(type: "text", nullable: false),
InstallScript = table.Column<string>(type: "text", nullable: false),
RequiredAllocations = table.Column<int>(type: "integer", nullable: false),
AllowDockerImageChange = table.Column<bool>(type: "boolean", nullable: false),
DefaultDockerImage = table.Column<int>(type: "integer", nullable: false),
ParseConfiguration = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Stars", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Servers",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
StarId = table.Column<int>(type: "integer", nullable: false),
NodeId = table.Column<int>(type: "integer", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
OwnerId = table.Column<int>(type: "integer", nullable: false),
StartupOverride = table.Column<string>(type: "text", nullable: true),
DockerImageIndex = table.Column<int>(type: "integer", nullable: false),
Cpu = table.Column<int>(type: "integer", nullable: false),
Memory = table.Column<int>(type: "integer", nullable: false),
Disk = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Servers", x => x.Id);
table.ForeignKey(
name: "FK_Servers_Nodes_NodeId",
column: x => x.NodeId,
principalSchema: "servers",
principalTable: "Nodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Servers_Stars_StarId",
column: x => x.StarId,
principalSchema: "servers",
principalTable: "Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "StarDockerImages",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
StarId = table.Column<int>(type: "integer", nullable: false),
DisplayName = table.Column<string>(type: "text", nullable: false),
Identifier = table.Column<string>(type: "text", nullable: false),
AutoPulling = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StarDockerImages", x => x.Id);
table.ForeignKey(
name: "FK_StarDockerImages_Stars_StarId",
column: x => x.StarId,
principalSchema: "servers",
principalTable: "Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "StarVariables",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
StarId = table.Column<int>(type: "integer", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
Key = table.Column<string>(type: "text", nullable: false),
DefaultValue = table.Column<string>(type: "text", nullable: false),
AllowViewing = table.Column<bool>(type: "boolean", nullable: false),
AllowEditing = table.Column<bool>(type: "boolean", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false),
Filter = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_StarVariables", x => x.Id);
table.ForeignKey(
name: "FK_StarVariables_Stars_StarId",
column: x => x.StarId,
principalSchema: "servers",
principalTable: "Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Allocations",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
NodeId = table.Column<int>(type: "integer", nullable: false),
ServerId = table.Column<int>(type: "integer", nullable: true),
IpAddress = table.Column<string>(type: "text", nullable: false),
Port = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Allocations", x => x.Id);
table.ForeignKey(
name: "FK_Allocations_Nodes_NodeId",
column: x => x.NodeId,
principalSchema: "servers",
principalTable: "Nodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Allocations_Servers_ServerId",
column: x => x.ServerId,
principalSchema: "servers",
principalTable: "Servers",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "ServerBackups",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CompletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Size = table.Column<long>(type: "bigint", nullable: false),
Successful = table.Column<bool>(type: "boolean", nullable: false),
Completed = table.Column<bool>(type: "boolean", nullable: false),
ServerId = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerBackups", x => x.Id);
table.ForeignKey(
name: "FK_ServerBackups_Servers_ServerId",
column: x => x.ServerId,
principalSchema: "servers",
principalTable: "Servers",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "ServerShares",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<int>(type: "integer", nullable: false),
ServerId = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Content = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerShares", x => x.Id);
table.ForeignKey(
name: "FK_ServerShares_Servers_ServerId",
column: x => x.ServerId,
principalSchema: "servers",
principalTable: "Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ServerVariables",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ServerId = table.Column<int>(type: "integer", nullable: false),
Key = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerVariables", x => x.Id);
table.ForeignKey(
name: "FK_ServerVariables_Servers_ServerId",
column: x => x.ServerId,
principalSchema: "servers",
principalTable: "Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Allocations_NodeId",
schema: "servers",
table: "Allocations",
column: "NodeId");
migrationBuilder.CreateIndex(
name: "IX_Allocations_ServerId",
schema: "servers",
table: "Allocations",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_ServerBackups_ServerId",
schema: "servers",
table: "ServerBackups",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_Servers_NodeId",
schema: "servers",
table: "Servers",
column: "NodeId");
migrationBuilder.CreateIndex(
name: "IX_Servers_StarId",
schema: "servers",
table: "Servers",
column: "StarId");
migrationBuilder.CreateIndex(
name: "IX_ServerShares_ServerId",
schema: "servers",
table: "ServerShares",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_ServerVariables_ServerId",
schema: "servers",
table: "ServerVariables",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_StarDockerImages_StarId",
schema: "servers",
table: "StarDockerImages",
column: "StarId");
migrationBuilder.CreateIndex(
name: "IX_StarVariables_StarId",
schema: "servers",
table: "StarVariables",
column: "StarId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Allocations",
schema: "servers");
migrationBuilder.DropTable(
name: "ServerBackups",
schema: "servers");
migrationBuilder.DropTable(
name: "ServerShares",
schema: "servers");
migrationBuilder.DropTable(
name: "ServerVariables",
schema: "servers");
migrationBuilder.DropTable(
name: "StarDockerImages",
schema: "servers");
migrationBuilder.DropTable(
name: "StarVariables",
schema: "servers");
migrationBuilder.DropTable(
name: "Servers",
schema: "servers");
migrationBuilder.DropTable(
name: "Nodes",
schema: "servers");
migrationBuilder.DropTable(
name: "Stars",
schema: "servers");
}
}
}

View File

@@ -1,526 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
[DbContext(typeof(ServersDataContext))]
partial class ServersDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("IpAddress")
.IsRequired()
.HasColumnType("text");
b.Property<int>("NodeId")
.HasColumnType("integer");
b.Property<int>("Port")
.HasColumnType("integer");
b.Property<int?>("ServerId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("ServerId");
b.ToTable("Allocations", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Fqdn")
.IsRequired()
.HasColumnType("text");
b.Property<int>("FtpPort")
.HasColumnType("integer");
b.Property<int>("HttpPort")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TokenId")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("UseSsl")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("Nodes", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Cpu")
.HasColumnType("integer");
b.Property<int>("Disk")
.HasColumnType("integer");
b.Property<int>("DockerImageIndex")
.HasColumnType("integer");
b.Property<int>("Memory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("NodeId")
.HasColumnType("integer");
b.Property<int>("OwnerId")
.HasColumnType("integer");
b.Property<int>("StarId")
.HasColumnType("integer");
b.Property<string>("StartupOverride")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("Completed")
.HasColumnType("boolean");
b.Property<DateTime>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("ServerId")
.HasColumnType("integer");
b.Property<long>("Size")
.HasColumnType("bigint");
b.Property<bool>("Successful")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerBackups", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerShare", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("ServerId")
.HasColumnType("integer");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerShares", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<int>("ServerId")
.HasColumnType("integer");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ServerId");
b.ToTable("ServerVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowDockerImageChange")
.HasColumnType("boolean");
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<int>("DefaultDockerImage")
.HasColumnType("integer");
b.Property<string>("DonateUrl")
.HasColumnType("text");
b.Property<string>("InstallDockerImage")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstallScript")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstallShell")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OnlineDetection")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ParseConfiguration")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RequiredAllocations")
.HasColumnType("integer");
b.Property<string>("StartupCommand")
.IsRequired()
.HasColumnType("text");
b.Property<string>("StopCommand")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UpdateUrl")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Stars", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AutoPulling")
.HasColumnType("boolean");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StarId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StarId");
b.ToTable("StarDockerImages", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowEditing")
.HasColumnType("boolean");
b.Property<bool>("AllowViewing")
.HasColumnType("boolean");
b.Property<string>("DefaultValue")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Filter")
.HasColumnType("text");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StarId")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StarId");
b.ToTable("StarVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node")
.WithMany("Allocations")
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Allocations")
.HasForeignKey("ServerId");
b.Navigation("Node");
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node")
.WithMany("Servers")
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany()
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Node");
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", null)
.WithMany("Backups")
.HasForeignKey("ServerId");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerShare", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Shares")
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("MoonlightServers.ApiServer.Models.ServerShareContent", "Content", b1 =>
{
b1.Property<int>("ServerShareId")
.HasColumnType("integer");
b1.HasKey("ServerShareId");
b1.ToTable("ServerShares", "servers");
b1.ToJson("Content");
b1.WithOwner()
.HasForeignKey("ServerShareId");
b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerShareContent+SharePermission", "Permissions", b2 =>
{
b2.Property<int>("ServerShareContentServerShareId")
.HasColumnType("integer");
b2.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
b2.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b2.Property<int>("Level")
.HasColumnType("integer");
b2.HasKey("ServerShareContentServerShareId", "__synthesizedOrdinal");
b2.ToTable("ServerShares", "servers");
b2.WithOwner()
.HasForeignKey("ServerShareContentServerShareId");
});
b1.Navigation("Permissions");
});
b.Navigation("Content")
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server")
.WithMany("Variables")
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany("DockerImages")
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b =>
{
b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star")
.WithMany("Variables")
.HasForeignKey("StarId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Star");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Navigation("Allocations");
b.Navigation("Servers");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Navigation("Allocations");
b.Navigation("Backups");
b.Navigation("Shares");
b.Navigation("Variables");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,70 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.ApiServer.Configuration;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Models;
namespace MoonlightServers.ApiServer.Database;
public class ServersDataContext : DbContext
{
public DbSet<Allocation> Allocations { get; set; }
public DbSet<Node> Nodes { get; set; }
public DbSet<Server> Servers { get; set; }
public DbSet<ServerBackup> ServerBackups { get; set; }
public DbSet<ServerShare> ServerShares { get; set; }
public DbSet<ServerVariable> ServerVariables { get; set; }
public DbSet<Star> Stars { get; set; }
public DbSet<StarDockerImage> StarDockerImages { get; set; }
public DbSet<StarVariable> StarVariables { get; set; }
private readonly AppConfiguration Configuration;
private readonly string Schema = "servers";
public ServersDataContext(AppConfiguration configuration)
{
Configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(optionsBuilder.IsConfigured)
return;
var database = Configuration.Database;
var connectionString = $"Host={database.Host};" +
$"Port={database.Port};" +
$"Database={database.Database};" +
$"Username={database.Username};" +
$"Password={database.Password}";
optionsBuilder.UseNpgsql(connectionString, builder =>
{
builder.MigrationsHistoryTable("MigrationsHistory", Schema);
});
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Model.SetDefaultSchema(Schema);
base.OnModelCreating(modelBuilder);
#region Shares
modelBuilder.Ignore<ServerShareContent>();
modelBuilder.Ignore<ServerShareContent.SharePermission>();
modelBuilder.Entity<ServerShare>(builder =>
{
builder.OwnsOne(x => x.Content, navigationBuilder =>
{
navigationBuilder.ToJson();
navigationBuilder.OwnsMany(x => x.Permissions);
});
});
#endregion
}
}

View File

@@ -1,19 +0,0 @@
using ServerState = MoonlightServers.Shared.Enums.ServerState;
namespace MoonlightServers.ApiServer.Extensions;
public static class ServerStateExtensions
{
public static ServerState ToServerPowerState(this DaemonShared.Enums.ServerState state)
{
return state switch
{
DaemonShared.Enums.ServerState.Installing => ServerState.Installing,
DaemonShared.Enums.ServerState.Stopping => ServerState.Stopping,
DaemonShared.Enums.ServerState.Online => ServerState.Online,
DaemonShared.Enums.ServerState.Starting => ServerState.Starting,
DaemonShared.Enums.ServerState.Offline => ServerState.Offline,
_ => ServerState.Offline
};
}
}

View File

@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Authentication;
namespace MoonlightServers.ApiServer.Helpers;
public class NodeAuthOptions : AuthenticationSchemeOptions
{
}

View File

@@ -1,78 +0,0 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
namespace MoonlightServers.ApiServer.Helpers;
public class NodeAuthScheme : AuthenticationHandler<NodeAuthOptions>
{
public NodeAuthScheme(IOptionsMonitor<NodeAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
public NodeAuthScheme(IOptionsMonitor<NodeAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(
options, logger, encoder)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.NoResult();
var authHeaderValue = Request.Headers["Authorization"].FirstOrDefault();
if (string.IsNullOrEmpty(authHeaderValue))
return AuthenticateResult.NoResult();
if (!authHeaderValue.Contains("Bearer "))
return AuthenticateResult.NoResult();
var tokenParts = authHeaderValue
.Replace("Bearer ", "")
.Trim()
.Split('.');
if (tokenParts.Length != 2)
return AuthenticateResult.NoResult();
var tokenId = tokenParts[0];
var token = tokenParts[1];
if (tokenId.Length != 6)
return AuthenticateResult.NoResult();
var nodeRepo = Context.RequestServices.GetRequiredService<DatabaseRepository<Node>>();
var node = await nodeRepo
.Get()
.FirstOrDefaultAsync(x => x.TokenId == tokenId);
if (node == null)
return AuthenticateResult.NoResult();
if (node.Token != token)
return AuthenticateResult.NoResult();
return AuthenticateResult.Success(
new AuthenticationTicket(
new ClaimsPrincipal(
new ClaimsIdentity(
[
new Claim("nodeId", node.Id.ToString())
],
"nodeAuthentication"
)
),
"nodeAuthentication"
)
);
}
}

View File

@@ -1,237 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.NodeAllocations;
using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes;
[ApiController]
[Route("api/admin/servers/nodes/{nodeId:int}/allocations")]
public class NodeAllocationsController : Controller
{
private readonly DatabaseRepository<Node> NodeRepository;
private readonly DatabaseRepository<Allocation> AllocationRepository;
public NodeAllocationsController(
DatabaseRepository<Node> nodeRepository,
DatabaseRepository<Allocation> allocationRepository
)
{
NodeRepository = nodeRepository;
AllocationRepository = allocationRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<ActionResult<CountedData<NodeAllocationResponse>>> GetAsync(
[FromRoute] int nodeId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var totalCount = await AllocationRepository
.Get()
.CountAsync(x => x.Node.Id == nodeId);
var allocations = await AllocationRepository
.Get()
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.Where(x => x.Node.Id == nodeId)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<NodeAllocationResponse>()
{
Items = allocations,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<ActionResult<NodeAllocationResponse>> GetSingleAsync([FromRoute] int nodeId, [FromRoute] int id)
{
var allocation = await AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.AsNoTracking()
.ProjectToAdminResponse()
.FirstOrDefaultAsync(x => x.Id == id);
if (allocation == null)
return Problem("No allocation with that id found", statusCode: 400);
return allocation;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
public async Task<ActionResult<NodeAllocationResponse>> CreateAsync(
[FromRoute] int nodeId,
[FromBody] CreateNodeAllocationRequest request
)
{
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (node == null)
return Problem("No node with that id found", statusCode: 404);
var allocation = AllocationMapper.ToAllocation(request);
var finalAllocation = await AllocationRepository.AddAsync(allocation);
return AllocationMapper.ToNodeAllocation(finalAllocation);
}
[HttpPatch("{id:int}")]
public async Task<ActionResult<NodeAllocationResponse>> UpdateAsync(
[FromRoute] int nodeId,
[FromRoute] int id,
[FromBody] UpdateNodeAllocationRequest request
)
{
var allocation = await AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.FirstOrDefaultAsync(x => x.Id == id);
if (allocation == null)
return Problem("No allocation with that id found", statusCode: 404);
AllocationMapper.Merge(request, allocation);
await AllocationRepository.UpdateAsync(allocation);
return AllocationMapper.ToNodeAllocation(allocation);
}
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteAsync([FromRoute] int nodeId, [FromRoute] int id)
{
var allocation = await AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.FirstOrDefaultAsync(x => x.Id == id);
if (allocation == null)
return Problem("No allocation with that id found", statusCode: 404);
await AllocationRepository.RemoveAsync(allocation);
return NoContent();
}
[HttpPost("range")]
public async Task<ActionResult> CreateRangeAsync(
[FromRoute] int nodeId,
[FromBody] CreateNodeAllocationRangeRequest request
)
{
if (request.Start > request.End)
return Problem("Invalid start and end specified", statusCode: 400);
if (request.End - request.Start == 0)
return Problem("Empty range specified", statusCode: 400);
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (node == null)
return Problem("No node with that id found", statusCode: 404);
var existingAllocations = await AllocationRepository
.Get()
.Where(x => x.Port >= request.Start && x.Port <= request.End &&
x.IpAddress == request.IpAddress)
.AsNoTracking()
.ToArrayAsync();
var ports = new List<int>();
for (var i = request.Start; i < request.End; i++)
{
// Skip existing allocations
if (existingAllocations.Any(x => x.Port == i))
continue;
ports.Add(i);
}
var allocations = ports
.Select(port => new Allocation()
{
IpAddress = request.IpAddress,
Port = port,
Node = node
})
.ToArray();
await AllocationRepository.RunTransactionAsync(async set => { await set.AddRangeAsync(allocations); });
return NoContent();
}
[HttpDelete("all")]
public async Task<ActionResult> DeleteAllAsync([FromRoute] int nodeId)
{
var allocations = AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.ToArray();
await AllocationRepository.RunTransactionAsync(set => { set.RemoveRange(allocations); });
return NoContent();
}
[HttpGet("free")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<ActionResult<CountedData<NodeAllocationResponse>>> GetFreeAsync(
[FromRoute] int nodeId,
[FromQuery] int startIndex,
[FromQuery] int count,
[FromQuery] int serverId = -1
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var node = NodeRepository
.Get()
.FirstOrDefault(x => x.Id == nodeId);
if (node == null)
return Problem("A node with this id could not be found", statusCode: 404);
var freeAllocationsQuery = AllocationRepository
.Get()
.OrderBy(x => x.Id)
.Where(x => x.Node.Id == node.Id)
.Where(x => x.Server == null || x.Server.Id == serverId);
var totalCount = await freeAllocationsQuery.CountAsync();
var allocations = await freeAllocationsQuery
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<NodeAllocationResponse>()
{
Items = allocations,
TotalCount = totalCount
};
}
}

View File

@@ -1,82 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Extended.Abstractions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes;
[ApiController]
[Route("api/admin/servers/nodes")]
public class NodeStatusController : Controller
{
private readonly DatabaseRepository<Node> NodeRepository;
private readonly NodeService NodeService;
public NodeStatusController(DatabaseRepository<Node> nodeRepository, NodeService nodeService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
}
[HttpGet("{nodeId:int}/system/status")]
[Authorize(Policy = "permissions:admin.servers.nodes.status")]
public async Task<ActionResult<NodeSystemStatusResponse>> GetStatusAsync([FromRoute] int nodeId)
{
var node = await GetNodeAsync(nodeId);
if (node.Value == null)
return node.Result ?? Problem("Unable to retrieve node");
NodeSystemStatusResponse response;
var sw = new Stopwatch();
sw.Start();
try
{
var statusResponse = await NodeService.GetSystemStatusAsync(node.Value);
sw.Stop();
response = new()
{
Version = statusResponse.Version,
RoundtripError = statusResponse.TripError,
RoundtripSuccess = statusResponse.TripSuccess,
RoundtripTime = statusResponse.TripTime + sw.Elapsed,
RoundtripRemoteFailure = !statusResponse.TripSuccess // When the remote trip failed, it's the remotes fault
};
}
catch (Exception e)
{
sw.Stop();
response = new()
{
Version = "Unknown",
RoundtripError = e.Message,
RoundtripSuccess = false,
RoundtripTime = sw.Elapsed,
RoundtripRemoteFailure = false
};
}
return response;
}
private async Task<ActionResult<Node>> GetNodeAsync(int nodeId)
{
var result = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (result == null)
return Problem("A node with this id could not be found", statusCode: 404);
return result;
}
}

View File

@@ -1,114 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Common;
using MoonCore.Helpers;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.Nodes;
using MoonlightServers.Shared.Http.Responses.Admin.Nodes;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes;
[ApiController]
[Route("api/admin/servers/nodes")]
public class NodesController : Controller
{
private readonly DatabaseRepository<Node> NodeRepository;
public NodesController(DatabaseRepository<Node> nodeRepository)
{
NodeRepository = nodeRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<ActionResult<CountedData<NodeResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var totalCount = await NodeRepository.Get().CountAsync();
var items = await NodeRepository
.Get()
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<NodeResponse>()
{
Items = items,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<ActionResult<NodeResponse>> GetSingleAsync([FromRoute] int id)
{
var node = await NodeRepository
.Get()
.AsNoTracking()
.ProjectToAdminResponse()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
return node;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
public async Task<ActionResult<NodeResponse>> CreateAsync([FromBody] CreateNodeRequest request)
{
var node = NodeMapper.ToNode(request);
node.TokenId = Formatter.GenerateString(6);
node.Token = Formatter.GenerateString(32);
var finalNode = await NodeRepository.AddAsync(node);
return NodeMapper.ToAdminNodeResponse(finalNode);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.update")]
public async Task<ActionResult<NodeResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateNodeRequest request)
{
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
NodeMapper.Merge(request, node);
await NodeRepository.UpdateAsync(node);
return NodeMapper.ToAdminNodeResponse(node);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.delete")]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
await NodeRepository.RemoveAsync(node);
return Ok();
}
}

View File

@@ -1,100 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes;
[ApiController]
[Route("api/admin/servers/nodes/{nodeId:int}/statistics")]
[Authorize(Policy = "permissions:admin.servers.nodes.statistics")]
public class StatisticsController : Controller
{
private readonly NodeService NodeService;
private readonly DatabaseRepository<Node> NodeRepository;
public StatisticsController(NodeService nodeService, DatabaseRepository<Node> nodeRepository)
{
NodeService = nodeService;
NodeRepository = nodeRepository;
}
[HttpGet]
[SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action", MessageId = "time: 1142ms",
Justification = "The daemon has an artificial delay of one second to calculate accurate cpu usage values")]
public async Task<ActionResult<StatisticsResponse>> GetAsync([FromRoute] int nodeId)
{
var node = await GetNodeAsync(nodeId);
if (node.Value == null)
return node.Result ?? Problem("Unable to retrieve node");
var statistics = await NodeService.GetStatisticsAsync(node.Value);
return new StatisticsResponse()
{
Cpu = new()
{
Model = statistics.Cpu.Model,
Usage = statistics.Cpu.Usage,
UsagePerCore = statistics.Cpu.UsagePerCore
},
Memory = new()
{
Available = statistics.Memory.Available,
Total = statistics.Memory.Total,
Cached = statistics.Memory.Cached,
Free = statistics.Memory.Free,
SwapFree = statistics.Memory.SwapFree,
SwapTotal = statistics.Memory.SwapTotal
},
Disks = statistics.Disks.Select(x => new StatisticsResponse.DiskData()
{
Device = x.Device,
DiskFree = x.DiskFree,
DiskTotal = x.DiskTotal,
InodesFree = x.InodesFree,
InodesTotal = x.InodesTotal,
MountPath = x.MountPath
}).ToArray()
};
}
[HttpGet("docker")]
public async Task<ActionResult<DockerStatisticsResponse>> GetDockerAsync([FromRoute] int nodeId)
{
var node = await GetNodeAsync(nodeId);
if (node.Value == null)
return node.Result ?? Problem("Unable to retrieve node");
var statistics = await NodeService.GetDockerStatisticsAsync(node.Value);
return new DockerStatisticsResponse()
{
BuildCacheReclaimable = statistics.BuildCacheReclaimable,
BuildCacheUsed = statistics.BuildCacheUsed,
ContainersReclaimable = statistics.ContainersReclaimable,
ContainersUsed = statistics.ContainersUsed,
ImagesReclaimable = statistics.ImagesReclaimable,
ImagesUsed = statistics.ImagesUsed,
Version = statistics.Version
};
}
private async Task<ActionResult<Node>> GetNodeAsync(int nodeId)
{
var result = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (result == null)
return Problem("A node with this id could not be found", statusCode: 404);
return result;
}
}

View File

@@ -1,66 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Responses.Admin.ServerVariables;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Servers;
[ApiController]
[Route("api/admin/servers")]
public class ServerVariablesController : Controller
{
private readonly DatabaseRepository<ServerVariable> VariableRepository;
private readonly DatabaseRepository<Server> ServerRepository;
public ServerVariablesController(
DatabaseRepository<ServerVariable> variableRepository,
DatabaseRepository<Server> serverRepository
)
{
VariableRepository = variableRepository;
ServerRepository = serverRepository;
}
[HttpGet("{serverId:int}/variables")]
[Authorize(Policy = "permissions:admin.servers.read")]
public async Task<ActionResult<CountedData<ServerVariableResponse>>> GetAsync(
[FromRoute] int serverId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var serverExists = await ServerRepository
.Get()
.AnyAsync(x => x.Id == serverId);
if (!serverExists)
return Problem("No server with this id found", statusCode: 404);
var query = VariableRepository
.Get()
.Where(x => x.Server.Id == serverId);
var totalCount = await query.CountAsync();
var variables = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<ServerVariableResponse>()
{
Items = variables,
TotalCount = totalCount
};
}
}

View File

@@ -1,330 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Requests.Admin.Servers;
using MoonlightServers.Shared.Http.Responses.Admin.Servers;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Servers;
[ApiController]
[Route("api/admin/servers")]
public class ServersController : Controller
{
private readonly DatabaseRepository<Star> StarRepository;
private readonly DatabaseRepository<Node> NodeRepository;
private readonly DatabaseRepository<Allocation> AllocationRepository;
private readonly DatabaseRepository<ServerVariable> VariableRepository;
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly ILogger<ServersController> Logger;
private readonly ServerService ServerService;
public ServersController(
DatabaseRepository<Star> starRepository,
DatabaseRepository<Node> nodeRepository,
DatabaseRepository<Allocation> allocationRepository,
DatabaseRepository<ServerVariable> variableRepository,
DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository,
ILogger<ServersController> logger,
ServerService serverService
)
{
StarRepository = starRepository;
NodeRepository = nodeRepository;
AllocationRepository = allocationRepository;
VariableRepository = variableRepository;
ServerRepository = serverRepository;
UserRepository = userRepository;
ServerService = serverService;
Logger = logger;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.read")]
public async Task<ActionResult<CountedData<ServerResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var totalCount = await ServerRepository.Get().CountAsync();
var servers = await ServerRepository
.Get()
.Include(x => x.Node)
.Include(x => x.Allocations)
.Include(x => x.Variables)
.Include(x => x.Star)
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<ServerResponse>()
{
Items = servers,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.read")]
public async Task<ActionResult<ServerResponse>> GetSingleAsync([FromRoute] int id)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.Include(x => x.Allocations)
.Include(x => x.Variables)
.Include(x => x.Star)
.AsNoTracking()
.Where(x => x.Id == id)
.ProjectToAdminResponse()
.FirstOrDefaultAsync();
if (server == null)
return Problem("No server with that id found", statusCode: 404);
return server;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.write")]
public async Task<ActionResult<ServerResponse>> CreateAsync([FromBody] CreateServerRequest request)
{
// Check if owner user exist
if (UserRepository.Get().All(x => x.Id != request.OwnerId))
return Problem("No user with this id found", statusCode: 400);
// Check if the star exists
var star = await StarRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.DockerImages)
.FirstOrDefaultAsync(x => x.Id == request.StarId);
if (star == null)
return Problem("No star with this id found", statusCode: 400);
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == request.NodeId);
if (node == null)
return Problem("No node with this id found", statusCode: 400);
var allocations = new List<Allocation>();
// Fetch specified allocations from the request
foreach (var allocationId in request.AllocationIds)
{
var allocation = await AllocationRepository
.Get()
.Where(x => x.Server == null)
.Where(x => x.Node.Id == node.Id)
.FirstOrDefaultAsync(x => x.Id == allocationId);
if (allocation == null)
continue;
allocations.Add(allocation);
}
// Check if the specified allocations are enough for the star
if (allocations.Count < star.RequiredAllocations)
{
var amountRequiredToSatisfy = star.RequiredAllocations - allocations.Count;
var freeAllocations = await AllocationRepository
.Get()
.Where(x => x.Server == null)
.Where(x => x.Node.Id == node.Id)
.Take(amountRequiredToSatisfy)
.ToArrayAsync();
allocations.AddRange(freeAllocations);
if (allocations.Count < star.RequiredAllocations)
{
return Problem(
$"Unable to find enough free allocations. Found: {allocations.Count}, Required: {star.RequiredAllocations}",
statusCode: 400
);
}
}
var server = ServerMapper.ToServer(request);
// Set allocations
server.Allocations = allocations;
// Variables
foreach (var variable in star.Variables)
{
var requestVar = request.Variables.FirstOrDefault(x => x.Key == variable.Key);
var serverVar = new ServerVariable()
{
Key = variable.Key,
Value = requestVar != null
? requestVar.Value
: variable.DefaultValue
};
server.Variables.Add(serverVar);
}
// Set relations
server.Node = node;
server.Star = star;
var finalServer = await ServerRepository.AddAsync(server);
try
{
await ServerService.SyncAsync(finalServer);
}
catch (Exception e)
{
Logger.LogError("Unable to sync server to node the server is assigned to: {e}", e);
// We are deleting the server from the database after the creation has failed
// to ensure we won't have a bugged server in the database which doesn't exist on the node
await ServerRepository.RemoveAsync(finalServer);
throw;
}
return ServerMapper.ToAdminServerResponse(finalServer);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.write")]
public async Task<ActionResult<ServerResponse>> UpdateAsync(
[FromRoute] int id,
[FromBody] UpdateServerRequest request
)
{
//TODO: Handle shrinking virtual disk
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.Include(x => x.Allocations)
.Include(x => x.Variables)
.Include(x => x.Star)
.FirstOrDefaultAsync(x => x.Id == id);
if (server == null)
return Problem("No server with that id found", statusCode: 404);
ServerMapper.Merge(request, server);
var allocations = new List<Allocation>();
// Fetch specified allocations from the request
foreach (var allocationId in request.AllocationIds)
{
var allocation = await AllocationRepository
.Get()
.Where(x => x.Server == null || x.Server.Id == server.Id)
.Where(x => x.Node.Id == server.Node.Id)
.FirstOrDefaultAsync(x => x.Id == allocationId);
// ^ This loads the allocations specified in the request.
// Valid allocations are either free ones or ones which are already allocated to this server
if (allocation == null)
continue;
allocations.Add(allocation);
}
// Check if the specified allocations are enough for the star
if (allocations.Count < server.Star.RequiredAllocations)
{
return Problem(
$"You need to specify at least {server.Star.RequiredAllocations} allocation(s)",
statusCode: 400
);
}
// Set allocations
server.Allocations = allocations;
// Process variables
foreach (var variable in request.Variables)
{
// Search server variable associated to the variable in the request
var serverVar = server.Variables
.FirstOrDefault(x => x.Key == variable.Key);
if (serverVar == null)
continue;
// Update value
serverVar.Value = variable.Value;
}
await ServerRepository.UpdateAsync(server);
// Notify the node about the changes
await ServerService.SyncAsync(server);
return ServerMapper.ToAdminServerResponse(server);
}
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteAsync([FromRoute] int id, [FromQuery] bool force = false)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.Include(x => x.Star)
.Include(x => x.Variables)
.Include(x => x.Backups)
.Include(x => x.Allocations)
.FirstOrDefaultAsync(x => x.Id == id);
if (server == null)
return Problem("No server with that id found", statusCode: 404);
server.Variables.Clear();
server.Backups.Clear();
server.Allocations.Clear();
try
{
// If the sync fails on the node and we aren't forcing the deletion,
// we don't want to delete it from the database yet
await ServerService.SyncDeleteAsync(server);
}
catch (Exception e)
{
if (force)
{
Logger.LogWarning(
"An error occured while syncing deletion of a server to the node. Continuing anyways. Error: {e}",
e
);
}
else
throw;
}
await ServerRepository.RemoveAsync(server);
return NoContent();
}
}

View File

@@ -1,159 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Common;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.StarDockerImages;
using MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
[ApiController]
[Route("api/admin/servers/stars/{starId:int}/dockerImages")]
public class StarDockerImagesController : Controller
{
private readonly DatabaseRepository<Star> StarRepository;
private readonly DatabaseRepository<StarDockerImage> DockerImageRepository;
public StarDockerImagesController(
DatabaseRepository<Star> starRepository,
DatabaseRepository<StarDockerImage> dockerImageRepository
)
{
StarRepository = starRepository;
DockerImageRepository = dockerImageRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<ActionResult<CountedData<StarDockerImageResponse>>> GetAsync(
[FromRoute] int starId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var query = DockerImageRepository
.Get()
.Where(x => x.Star.Id == starId);
var totalCount = await query.CountAsync();
var dockerImages = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<StarDockerImageResponse>()
{
Items = dockerImages,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.read")]
public async Task<ActionResult<StarDockerImageResponse>> GetSingleAsync([FromRoute] int starId, [FromRoute] int id)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var dockerImage = await DockerImageRepository
.Get()
.Where(x => x.Id == id && x.Star.Id == starId)
.ProjectToAdminResponse()
.FirstOrDefaultAsync();
if (dockerImage == null)
return Problem("No star docker image with this id found", statusCode: 404);
return dockerImage;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.stars.write")]
public async Task<ActionResult<StarDockerImageResponse>> CreateAsync(
[FromRoute] int starId,
[FromBody] CreateStarDockerImageRequest request
)
{
var star = await StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == starId);
if (star == null)
return Problem("No star with this id found", statusCode: 404);
var dockerImage = DockerImageMapper.ToDockerImage(request);
dockerImage.Star = star;
var finalDockerImage = await DockerImageRepository.AddAsync(dockerImage);
return DockerImageMapper.ToAdminResponse(finalDockerImage);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.write")]
public async Task<ActionResult<StarDockerImageResponse>> UpdateAsync(
[FromRoute] int starId,
[FromRoute] int id,
[FromBody] UpdateStarDockerImageRequest request
)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var dockerImage = await DockerImageRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
if (dockerImage == null)
return Problem("No star docker image with this id found", statusCode: 404);
DockerImageMapper.Merge(request, dockerImage);
await DockerImageRepository.UpdateAsync(dockerImage);
return DockerImageMapper.ToAdminResponse(dockerImage);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.write")]
public async Task<ActionResult> DeleteAsync([FromRoute] int starId, [FromRoute] int id)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var dockerImage = await DockerImageRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
if (dockerImage == null)
return Problem("No star docker image with this id found", statusCode: 404);
await DockerImageRepository.RemoveAsync(dockerImage);
return NoContent();
}
}

View File

@@ -1,49 +0,0 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Exceptions;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Http.Responses.Admin.Stars;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
[ApiController]
[Route("api/admin/servers/stars")]
public class StarImportExportController : Controller
{
private readonly StarImportExportService ImportExportService;
public StarImportExportController(StarImportExportService importExportService)
{
ImportExportService = importExportService;
}
[HttpGet("{starId:int}/export")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<ActionResult> ExportAsync([FromRoute] int starId)
{
var exportedStar = await ImportExportService.ExportAsync(starId);
return Content(exportedStar, "application/json");
}
[HttpPost("import")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarResponse> ImportAsync()
{
if (Request.Form.Files.Count == 0)
throw new HttpApiException("No file to import provided", 400);
if (Request.Form.Files.Count > 1)
throw new HttpApiException("Only one file to import allowed", 400);
var file = Request.Form.Files[0];
await using var stream = file.OpenReadStream();
using var sr = new StreamReader(stream, Encoding.UTF8);
var content = await sr.ReadToEndAsync();
var star = await ImportExportService.ImportAsync(content);
return StarMapper.ToAdminResponse(star);
}
}

View File

@@ -1,160 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Common;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.StarVariables;
using MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
[ApiController]
[Route("api/admin/servers/stars/{starId:int}/variables")]
public class StarVariablesController : Controller
{
private readonly DatabaseRepository<Star> StarRepository;
private readonly DatabaseRepository<StarVariable> VariableRepository;
public StarVariablesController(
DatabaseRepository<Star> starRepository,
DatabaseRepository<StarVariable> variableRepository)
{
StarRepository = starRepository;
VariableRepository = variableRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<ActionResult<CountedData<StarVariableResponse>>> GetAsync(
[FromRoute] int starId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
return Problem("No star with this id found", statusCode: 404);
var query = VariableRepository
.Get()
.Where(x => x.Star.Id == starId);
var totalCount = await query.CountAsync();
var variables = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<StarVariableResponse>()
{
Items = variables,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<StarVariableResponse> GetSingleAsync(
[FromRoute] int starId,
[FromRoute] int id
)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
throw new HttpApiException("No star with this id found", 404);
var starVariable = await VariableRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
if (starVariable == null)
throw new HttpApiException("No variable with this id found", 404);
return StarVariableMapper.ToAdminResponse(starVariable);
}
[HttpPost("")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarVariableResponse> CreateAsync([FromRoute] int starId,
[FromBody] CreateStarVariableRequest request)
{
var star = StarRepository
.Get()
.FirstOrDefault(x => x.Id == starId);
if (star == null)
throw new HttpApiException("No star with this id found", 404);
var starVariable = StarVariableMapper.ToStarVariable(request);
starVariable.Star = star;
await VariableRepository.AddAsync(starVariable);
return StarVariableMapper.ToAdminResponse(starVariable);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.update")]
public async Task<StarVariableResponse> UpdateAsync(
[FromRoute] int starId,
[FromRoute] int id,
[FromBody] UpdateStarVariableRequest request
)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
throw new HttpApiException("No star with this id found", 404);
var starVariable = await VariableRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
if (starVariable == null)
throw new HttpApiException("No variable with this id found", 404);
StarVariableMapper.Merge(request, starVariable);
await VariableRepository.UpdateAsync(starVariable);
return StarVariableMapper.ToAdminResponse(starVariable);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
public async Task DeleteAsync([FromRoute] int starId, [FromRoute] int id)
{
var starExists = StarRepository
.Get()
.Any(x => x.Id == starId);
if (!starExists)
throw new HttpApiException("No star with this id found", 404);
var starVariable = await VariableRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
if (starVariable == null)
throw new HttpApiException("No variable with this id found", 404);
await VariableRepository.RemoveAsync(starVariable);
}
}

View File

@@ -1,128 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.Shared.Http.Requests.Admin.Stars;
using MoonlightServers.Shared.Http.Responses.Admin.Stars;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
[ApiController]
[Route("api/admin/servers/stars")]
public class StarsController : Controller
{
private readonly DatabaseRepository<Star> StarRepository;
public StarsController(DatabaseRepository<Star> starRepository)
{
StarRepository = starRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.stars.read")]
public async Task<ActionResult<CountedData<StarResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var totalCount = await StarRepository.Get().CountAsync();
var stars = await StarRepository
.Get()
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToAdminResponse()
.ToArrayAsync();
return new CountedData<StarResponse>()
{
Items = stars,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.read")]
public async Task<ActionResult<StarResponse>> GetSingleAsync([FromRoute] int id)
{
var star = await StarRepository
.Get()
.AsNoTracking()
.ProjectToAdminResponse()
.FirstOrDefaultAsync(x => x.Id == id);
if (star == null)
return Problem("No star with that id found", statusCode: 404);
return star;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<ActionResult<StarResponse>> CreateAsync([FromBody] CreateStarRequest request)
{
var star = StarMapper.ToStar(request);
// Default values
star.DonateUrl = null;
star.UpdateUrl = null;
star.Version = "1.0.0";
star.StartupCommand = "echo Starting up :)";
star.StopCommand = "^C";
star.OnlineDetection = "Online text";
star.InstallShell = "/bin/bash";
star.InstallDockerImage = "debian:latest";
star.InstallScript = "echo Installing...";
star.RequiredAllocations = 1;
star.AllowDockerImageChange = false;
star.DefaultDockerImage = -1;
star.ParseConfiguration = "[]";
var finalStar = await StarRepository.AddAsync(star);
return StarMapper.ToAdminResponse(finalStar);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.update")]
public async Task<ActionResult<StarResponse>> UpdateAsync(
[FromRoute] int id,
[FromBody] UpdateStarRequest request
)
{
var star = await StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (star == null)
return Problem("No star with that id found", statusCode: 404);
StarMapper.Merge(request, star);
await StarRepository.UpdateAsync(star);
return StarMapper.ToAdminResponse(star);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var star = await StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (star == null)
return Problem("No star with that id found", statusCode: 404);
await StarRepository.RemoveAsync(star);
return NoContent();
}
}

View File

@@ -1,227 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.DaemonShared.Enums;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Files;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers/{serverId:int}/files")]
public class FilesController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly ServerFileSystemService ServerFileSystemService;
private readonly NodeService NodeService;
private readonly ServerAuthorizeService AuthorizeService;
public FilesController(
DatabaseRepository<Server> serverRepository,
ServerFileSystemService serverFileSystemService,
NodeService nodeService,
ServerAuthorizeService authorizeService
)
{
ServerRepository = serverRepository;
ServerFileSystemService = serverFileSystemService;
NodeService = nodeService;
AuthorizeService = authorizeService;
}
[HttpGet("list")]
public async Task<ActionResult<ServerFilesEntryResponse[]>> ListAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.Read);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var entries = await ServerFileSystemService.ListAsync(server.Value, path);
return entries.Select(x => new ServerFilesEntryResponse()
{
Name = x.Name,
Size = x.Size,
IsFolder = x.IsFolder,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
}).ToArray();
}
[HttpPost("move")]
public async Task<ActionResult> MoveAsync([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath)
{
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerFileSystemService.MoveAsync(server.Value, oldPath, newPath);
return NoContent();
}
[HttpDelete("delete")]
public async Task<ActionResult> DeleteAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerFileSystemService.DeleteAsync(server.Value, path);
return NoContent();
}
[HttpPost("mkdir")]
public async Task<ActionResult> MkdirAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerFileSystemService.MkdirAsync(server.Value, path);
return NoContent();
}
[HttpPost("touch")]
public async Task<ActionResult> TouchAsync([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerFileSystemService.MkdirAsync(server.Value, path);
return NoContent();
}
[HttpGet("upload")]
public async Task<ActionResult<ServerFilesUploadResponse>> UploadAsync([FromRoute] int serverId)
{
var serverResult = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
var accessToken = NodeService.CreateAccessToken(
server.Node,
parameters =>
{
parameters.Add("type", "upload");
parameters.Add("serverId", server.Id);
},
TimeSpan.FromMinutes(1)
);
var url = "";
url += server.Node.UseSsl ? "https://" : "http://";
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/";
url += $"api/servers/upload?access_token={accessToken}";
return new ServerFilesUploadResponse()
{
UploadUrl = url
};
}
[HttpGet("download")]
public async Task<ActionResult<ServerFilesDownloadResponse>> DownloadAsync([FromRoute] int serverId, [FromQuery] string path)
{
var serverResult = await GetServerByIdAsync(serverId, ServerPermissionLevel.Read);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
var accessToken = NodeService.CreateAccessToken(
server.Node,
parameters =>
{
parameters.Add("type", "download");
parameters.Add("path", path);
parameters.Add("serverId", server.Id);
},
TimeSpan.FromMinutes(1)
);
var url = "";
url += server.Node.UseSsl ? "https://" : "http://";
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/";
url += $"api/servers/download?access_token={accessToken}";
return new ServerFilesDownloadResponse()
{
DownloadUrl = url
};
}
[HttpPost("compress")]
public async Task<ActionResult> CompressAsync([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request)
{
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
if (!Enum.TryParse(request.Type, true, out CompressType type))
return Problem("Invalid compress type provided", statusCode: 400);
await ServerFileSystemService.CompressAsync(server.Value, type, request.Items, request.Destination);
return Ok();
}
[HttpPost("decompress")]
public async Task<ActionResult> DecompressAsync([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request)
{
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
if (!Enum.TryParse(request.Type, true, out CompressType type))
return Problem("Invalid decompress type provided", statusCode: 400);
await ServerFileSystemService.DecompressAsync(server.Value, type, request.Path, request.Destination);
return NoContent();
}
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId, ServerPermissionLevel level)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
ServerPermissionConstants.Files,
level
);
if (!authorizeResult.Succeeded)
{
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
statusCode: 403
);
}
return server;
}
}

View File

@@ -1,97 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[ApiController]
[Authorize]
[Route("api/client/servers/{serverId:int}")]
public class PowerController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly ServerService ServerService;
private readonly ServerAuthorizeService AuthorizeService;
public PowerController(
DatabaseRepository<Server> serverRepository,
ServerService serverService,
ServerAuthorizeService authorizeService
)
{
ServerRepository = serverRepository;
ServerService = serverService;
AuthorizeService = authorizeService;
}
[HttpPost("start")]
[Authorize]
public async Task<ActionResult> StartAsync([FromRoute] int serverId)
{
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.StartAsync(server.Value);
return NoContent();
}
[HttpPost("stop")]
[Authorize]
public async Task<ActionResult> StopAsync([FromRoute] int serverId)
{
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.StopAsync(server.Value);
return NoContent();
}
[HttpPost("kill")]
[Authorize]
public async Task<ActionResult> KillAsync([FromRoute] int serverId)
{
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.KillAsync(server.Value);
return NoContent();
}
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
ServerPermissionConstants.Power,
ServerPermissionLevel.ReadWrite
);
if (!authorizeResult.Succeeded)
{
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
statusCode: 403
);
}
return server;
}
}

View File

@@ -1,380 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Extensions;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Allocations;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers")]
public class ServersController : Controller
{
private readonly ServerService ServerService;
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<ServerShare> ShareRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly NodeService NodeService;
private readonly ServerAuthorizeService AuthorizeService;
public ServersController(
DatabaseRepository<Server> serverRepository,
NodeService nodeService,
ServerService serverService,
ServerAuthorizeService authorizeService,
DatabaseRepository<ServerShare> shareRepository,
DatabaseRepository<User> userRepository
)
{
ServerRepository = serverRepository;
NodeService = nodeService;
ServerService = serverService;
AuthorizeService = authorizeService;
ShareRepository = shareRepository;
UserRepository = userRepository;
}
[HttpGet]
public async Task<ActionResult<CountedData<ServerDetailResponse>>> GetAllAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var userIdClaim = User.FindFirstValue("UserId");
if (string.IsNullOrEmpty(userIdClaim))
return Problem("Only users are able to use this endpoint", statusCode: 400);
var userId = int.Parse(userIdClaim);
var query = ServerRepository
.Get()
.Include(x => x.Allocations)
.Include(x => x.Star)
.Include(x => x.Node)
.Where(x => x.OwnerId == userId);
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ToArrayAsync();
var mappedItems = items.Select(x => new ServerDetailResponse()
{
Id = x.Id,
Name = x.Name,
NodeName = x.Node.Name,
StarName = x.Star.Name,
Cpu = x.Cpu,
Memory = x.Memory,
Disk = x.Disk,
Allocations = x.Allocations.Select(y => new AllocationDetailResponse()
{
Id = y.Id,
Port = y.Port,
IpAddress = y.IpAddress
}).ToArray()
}).ToArray();
return new CountedData<ServerDetailResponse>()
{
Items = mappedItems,
TotalCount = totalCount
};
}
[HttpGet("shared")]
public async Task<ActionResult<CountedData<ServerDetailResponse>>> GetAllSharedAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var userIdClaim = User.FindFirstValue("UserId");
if (string.IsNullOrEmpty(userIdClaim))
return Problem("Only users are able to use this endpoint", statusCode: 400);
var userId = int.Parse(userIdClaim);
var query = ShareRepository
.Get()
.Include(x => x.Server)
.ThenInclude(x => x.Node)
.Include(x => x.Server)
.ThenInclude(x => x.Star)
.Include(x => x.Server)
.ThenInclude(x => x.Allocations)
.Where(x => x.UserId == userId);
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.ToArrayAsync();
var ownerIds = items
.Select(x => x.Server.OwnerId)
.Distinct()
.ToArray();
var owners = await UserRepository
.Get()
.Where(x => ownerIds.Contains(x.Id))
.ToArrayAsync();
var mappedItems = items.Select(x => new ServerDetailResponse()
{
Id = x.Server.Id,
Name = x.Server.Name,
NodeName = x.Server.Node.Name,
StarName = x.Server.Star.Name,
Cpu = x.Server.Cpu,
Memory = x.Server.Memory,
Disk = x.Server.Disk,
Allocations = x.Server.Allocations.Select(y => new AllocationDetailResponse()
{
Id = y.Id,
Port = y.Port,
IpAddress = y.IpAddress
}).ToArray(),
Share = new()
{
SharedBy = owners.First(y => y.Id == x.Server.OwnerId).Username,
Permissions = ShareMapper.MapToPermissionLevels(x.Content)
}
}).ToArray();
return new CountedData<ServerDetailResponse>()
{
Items = mappedItems,
TotalCount = totalCount
};
}
[HttpGet("{serverId:int}")]
public async Task<ActionResult<ServerDetailResponse>> GetAsync([FromRoute] int serverId)
{
var server = await ServerRepository
.Get()
.Include(x => x.Allocations)
.Include(x => x.Star)
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var authorizationResult = await AuthorizeService.AuthorizeAsync(
User,
server,
String.Empty,
ServerPermissionLevel.None
);
if (!authorizationResult.Succeeded)
{
return Problem(
authorizationResult.Message ?? "No server with this id found",
statusCode: 404
);
}
// Create mapped response
var response = new ServerDetailResponse()
{
Id = server.Id,
Name = server.Name,
NodeName = server.Node.Name,
StarName = server.Star.Name,
Cpu = server.Cpu,
Memory = server.Memory,
Disk = server.Disk,
Allocations = server.Allocations.Select(y => new AllocationDetailResponse()
{
Id = y.Id,
Port = y.Port,
IpAddress = y.IpAddress
}).ToArray()
};
// Handle requests on shared servers
if (authorizationResult.Share != null)
{
var owner = await UserRepository
.Get()
.FirstAsync(x => x.Id == server.OwnerId);
response.Share = new()
{
SharedBy = owner.Username,
Permissions = ShareMapper.MapToPermissionLevels(authorizationResult.Share.Content)
};
}
return response;
}
[HttpGet("{serverId:int}/status")]
public async Task<ActionResult<ServerStatusResponse>> GetStatusAsync([FromRoute] int serverId)
{
var server = await GetServerByIdAsync(
serverId,
ServerPermissionConstants.Console,
ServerPermissionLevel.None
);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var status = await ServerService.GetStatusAsync(server.Value);
return new ServerStatusResponse()
{
State = status.State.ToServerPowerState()
};
}
[HttpGet("{serverId:int}/ws")]
public async Task<ActionResult<ServerWebSocketResponse>> GetWebSocketAsync([FromRoute] int serverId)
{
var serverResult = await GetServerByIdAsync(
serverId,
ServerPermissionConstants.Console,
ServerPermissionLevel.Read
);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
// TODO: Handle transparent node proxy
var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
{
parameters.Add("type", "websocket");
parameters.Add("serverId", server.Id);
}, TimeSpan.FromMinutes(15)); // TODO: Configurable
var url = "";
url += server.Node.UseSsl ? "https://" : "http://";
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/ws";
return new ServerWebSocketResponse()
{
Target = url,
AccessToken = accessToken
};
}
[HttpGet("{serverId:int}/logs")]
public async Task<ActionResult<ServerLogsResponse>> GetLogsAsync([FromRoute] int serverId)
{
var server = await GetServerByIdAsync(
serverId,
ServerPermissionConstants.Console,
ServerPermissionLevel.Read
);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var logs = await ServerService.GetLogsAsync(server.Value);
return new ServerLogsResponse()
{
Messages = logs.Messages
};
}
[HttpGet("{serverId:int}/stats")]
public async Task<ActionResult<ServerStatsResponse>> GetStatsAsync([FromRoute] int serverId)
{
var server = await GetServerByIdAsync(
serverId,
ServerPermissionConstants.Console,
ServerPermissionLevel.Read
);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var stats = await ServerService.GetStatsAsync(server.Value);
return new ServerStatsResponse()
{
CpuUsage = stats.CpuUsage,
MemoryUsage = stats.MemoryUsage,
NetworkRead = stats.NetworkRead,
NetworkWrite = stats.NetworkWrite,
IoRead = stats.IoRead,
IoWrite = stats.IoWrite
};
}
[HttpPost("{serverId:int}/command")]
public async Task<ActionResult> CommandAsync([FromRoute] int serverId, [FromBody] ServerCommandRequest request)
{
var server = await GetServerByIdAsync(
serverId,
ServerPermissionConstants.Console,
ServerPermissionLevel.ReadWrite
);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.RunCommandAsync(server.Value, request.Command);
return NoContent();
}
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId, string permissionId,
ServerPermissionLevel level)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.AuthorizeAsync(User, server, permissionId, level);
if (!authorizeResult.Succeeded)
{
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
statusCode: 403
);
}
return server;
}
}

View File

@@ -1,71 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers")]
public class SettingsController : Controller
{
private readonly ServerService ServerService;
private readonly DatabaseRepository<Server> ServerRepository;
private readonly ServerAuthorizeService AuthorizeService;
public SettingsController(
ServerService serverService,
DatabaseRepository<Server> serverRepository,
ServerAuthorizeService authorizeService
)
{
ServerService = serverService;
ServerRepository = serverRepository;
AuthorizeService = authorizeService;
}
[HttpPost("{serverId:int}/install")]
[Authorize]
public async Task<ActionResult> InstallAsync([FromRoute] int serverId)
{
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
await ServerService.InstallAsync(server.Value);
return NoContent();
}
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
ServerPermissionConstants.Settings,
ServerPermissionLevel.ReadWrite
);
if (!authorizeResult.Succeeded)
{
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
statusCode: 403
);
}
return server;
}
}

View File

@@ -1,251 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Common;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Mappers;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Shares;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Shares;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers/{serverId:int}/shares")]
public class SharesController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<ServerShare> ShareRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly ServerAuthorizeService AuthorizeService;
public SharesController(
DatabaseRepository<Server> serverRepository,
DatabaseRepository<ServerShare> shareRepository,
DatabaseRepository<User> userRepository,
ServerAuthorizeService authorizeService
)
{
ServerRepository = serverRepository;
ShareRepository = shareRepository;
UserRepository = userRepository;
AuthorizeService = authorizeService;
}
[HttpGet]
public async Task<ActionResult<CountedData<ServerShareResponse>>> GetAllAsync(
[FromRoute] int serverId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var query = ShareRepository
.Get()
.Where(x => x.Server.Id == server.Value.Id);
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.ToArrayAsync();
var userIds = items
.Select(x => x.UserId)
.Distinct()
.ToArray();
var users = await UserRepository
.Get()
.Where(x => userIds.Contains(x.Id))
.ToArrayAsync();
var mappedItems = items.Select(x => new ServerShareResponse()
{
Id = x.Id,
Username = users.First(y => y.Id == x.UserId).Username,
Permissions = ShareMapper.MapToPermissionLevels(x.Content)
}).ToArray();
return new CountedData<ServerShareResponse>()
{
Items = mappedItems,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
public async Task<ActionResult<ServerShareResponse>> GetAsync(
[FromRoute] int serverId,
[FromRoute] int id
)
{
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id);
if (share == null)
return Problem("A share with that id cannot be found", statusCode: 404);
var user = await UserRepository
.Get()
.FirstAsync(x => x.Id == share.UserId);
var mappedItem = new ServerShareResponse()
{
Id = share.Id,
Username = user.Username,
Permissions = ShareMapper.MapToPermissionLevels(share.Content)
};
return mappedItem;
}
[HttpPost]
public async Task<ActionResult<ServerShareResponse>> CreateAsync(
[FromRoute] int serverId,
[FromBody] CreateShareRequest request
)
{
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Username == request.Username);
if (user == null)
return Problem("A user with that username could not be found", statusCode: 400);
var share = new ServerShare()
{
Server = server.Value,
Content = ShareMapper.MapToServerShareContent(request.Permissions),
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
UserId = user.Id
};
var finalShare = await ShareRepository.AddAsync(share);
var mappedItem = new ServerShareResponse()
{
Id = finalShare.Id,
Username = user.Username,
Permissions = ShareMapper.MapToPermissionLevels(finalShare.Content)
};
return mappedItem;
}
[HttpPatch("{id:int}")]
public async Task<ActionResult<ServerShareResponse>> UpdateAsync(
[FromRoute] int serverId,
[FromRoute] int id,
[FromBody] UpdateShareRequest request
)
{
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id);
if (share == null)
return Problem("A share with that id cannot be found", statusCode: 404);
share.Content = ShareMapper.MapToServerShareContent(request.Permissions);
share.UpdatedAt = DateTime.UtcNow;
await ShareRepository.UpdateAsync(share);
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == share.UserId);
if (user == null)
return Problem("A user with that id could not be found", statusCode: 400);
var mappedItem = new ServerShareResponse()
{
Id = share.Id,
Username = user.Username,
Permissions = ShareMapper.MapToPermissionLevels(share.Content)
};
return mappedItem;
}
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteAsync(
[FromRoute] int serverId,
[FromRoute] int id
)
{
var server = await GetServerByIdAsync(serverId);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id);
if (share == null)
return Problem("A share with that id cannot be found", statusCode: 404);
await ShareRepository.RemoveAsync(share);
return NoContent();
}
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId)
{
var server = await ServerRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
ServerPermissionConstants.Shares,
ServerPermissionLevel.ReadWrite
);
if (!authorizeResult.Succeeded)
{
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
statusCode: 403
);
}
return server;
}
}

View File

@@ -1,205 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Common;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Constants;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Variables;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Variables;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[Authorize]
[ApiController]
[Route("api/client/servers/{serverId:int}/variables")]
public class VariablesController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<ServerVariable> ServerVariableRepository;
private readonly DatabaseRepository<StarVariable> StarVariableRepository;
private readonly ServerAuthorizeService AuthorizeService;
public VariablesController(
DatabaseRepository<Server> serverRepository,
ServerAuthorizeService authorizeService,
DatabaseRepository<ServerVariable> serverVariableRepository,
DatabaseRepository<StarVariable> starVariableRepository
)
{
ServerRepository = serverRepository;
AuthorizeService = authorizeService;
ServerVariableRepository = serverVariableRepository;
StarVariableRepository = starVariableRepository;
}
[HttpGet]
public async Task<ActionResult<CountedData<ServerVariableDetailResponse>>> GetAsync(
[FromRoute] int serverId,
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
var server = await GetServerByIdAsync(serverId, ServerPermissionLevel.Read);
if (server.Value == null)
return server.Result ?? Problem("Unable to retrieve server");
var query = StarVariableRepository
.Get()
.Where(x => x.Star.Id == server.Value.Star.Id);
var totalCount = await query.CountAsync();
var starVariables = await query
.OrderBy(x => x.Id)
.Skip(startIndex)
.Take(count)
.ToArrayAsync();
var starVariableKeys = starVariables
.Select(x => x.Key)
.ToArray();
var serverVariables = await ServerVariableRepository
.Get()
.Where(x => x.Server.Id == server.Value.Id && starVariableKeys.Contains(x.Key))
.ToArrayAsync();
var responses = starVariables.Select(starVariable =>
{
var serverVariable = serverVariables.First(x => x.Key == starVariable.Key);
return new ServerVariableDetailResponse()
{
Key = starVariable.Key,
Value = serverVariable.Value,
Type = starVariable.Type,
Name = starVariable.Name,
Description = starVariable.Description,
Filter = starVariable.Filter
};
}).ToArray();
return new CountedData<ServerVariableDetailResponse>()
{
Items = responses,
TotalCount = totalCount
};
}
[HttpPut]
public async Task<ActionResult<ServerVariableDetailResponse>> UpdateSingleAsync(
[FromRoute] int serverId,
[FromBody] UpdateServerVariableRequest request
)
{
// TODO: Handle filter
var serverResult = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
var serverVariable = server.Variables.FirstOrDefault(x => x.Key == request.Key);
var starVariable = server.Star.Variables.FirstOrDefault(x => x.Key == request.Key);
if (serverVariable == null || starVariable == null)
throw new HttpApiException($"No variable with the key found: {request.Key}", 400);
serverVariable.Value = request.Value;
await ServerRepository.UpdateAsync(server);
return new ServerVariableDetailResponse()
{
Key = starVariable.Key,
Value = serverVariable.Value,
Type = starVariable.Type,
Name = starVariable.Name,
Description = starVariable.Description,
Filter = starVariable.Filter
};
}
[HttpPatch]
public async Task<ActionResult<ServerVariableDetailResponse[]>> UpdateAsync(
[FromRoute] int serverId,
[FromBody] UpdateServerVariableRangeRequest request
)
{
var serverResult = await GetServerByIdAsync(serverId, ServerPermissionLevel.ReadWrite);
if (serverResult.Value == null)
return serverResult.Result ?? Problem("Unable to retrieve server");
var server = serverResult.Value;
foreach (var variable in request.Variables)
{
// TODO: Handle filter
var serverVariable = server.Variables.FirstOrDefault(x => x.Key == variable.Key);
var starVariable = server.Star.Variables.FirstOrDefault(x => x.Key == variable.Key);
if (serverVariable == null || starVariable == null)
throw new HttpApiException($"No variable with the key found: {variable.Key}", 400);
serverVariable.Value = variable.Value;
}
await ServerRepository.UpdateAsync(server);
return request.Variables.Select(requestVariable =>
{
var serverVariable = server.Variables.First(x => x.Key == requestVariable.Key);
var starVariable = server.Star.Variables.First(x => x.Key == requestVariable.Key);
return new ServerVariableDetailResponse()
{
Key = starVariable.Key,
Value = serverVariable.Value,
Type = starVariable.Type,
Name = starVariable.Name,
Description = starVariable.Description,
Filter = starVariable.Filter
};
}).ToArray();
}
private async Task<ActionResult<Server>> GetServerByIdAsync(int serverId, ServerPermissionLevel level)
{
var server = await ServerRepository
.Get()
.Include(x => x.Star)
.ThenInclude(x => x.Variables)
.Include(x => x.Variables)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var authorizeResult = await AuthorizeService.AuthorizeAsync(
User, server,
ServerPermissionConstants.Variables,
level
);
if (!authorizeResult.Succeeded)
{
return Problem(
authorizeResult.Message ?? "No permission for the requested resource",
statusCode: 403
);
}
return server;
}
}

View File

@@ -1,13 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MoonlightServers.ApiServer.Http.Controllers.Remote.Nodes;
[ApiController]
[Route("api/remote/server/node")]
[Authorize(AuthenticationSchemes = "nodeAuthentication")]
public class NodeTripController : Controller
{
[HttpGet("trip")]
public Task GetAsync() => Task.CompletedTask;
}

View File

@@ -1,196 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MoonCore.Common;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.ApiServer.Http.Controllers.Remote;
[ApiController]
[Route("api/remote/servers")]
[Authorize(AuthenticationSchemes = "nodeAuthentication")]
public class ServersController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<Node> NodeRepository;
private readonly ILogger<ServersController> Logger;
public ServersController(
DatabaseRepository<Server> serverRepository,
DatabaseRepository<Node> nodeRepository,
ILogger<ServersController> logger
)
{
ServerRepository = serverRepository;
NodeRepository = nodeRepository;
Logger = logger;
}
[HttpGet]
public async Task<ActionResult<CountedData<ServerDataResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count
)
{
if (count > 100)
return Problem("Only 100 items can be fetched at a time", statusCode: 400);
// Load the node via the id
var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value);
var node = await NodeRepository
.Get()
.FirstAsync(x => x.Id == nodeId);
var total = await ServerRepository
.Get()
.Where(x => x.Node.Id == node.Id)
.CountAsync();
var servers = await ServerRepository
.Get()
.Where(x => x.Node.Id == node.Id)
.Include(x => x.Star)
.ThenInclude(x => x.DockerImages)
.Include(x => x.Variables)
.Include(x => x.Allocations)
.Skip(startIndex)
.Take(count)
.ToArrayAsync();
var serverData = new List<ServerDataResponse>();
foreach (var server in servers)
{
var convertedData = ConvertToServerData(server);
if (convertedData == null)
continue;
serverData.Add(convertedData);
}
return new CountedData<ServerDataResponse>()
{
Items = serverData.ToArray(),
TotalCount = total
};
}
[HttpGet("{id:int}")]
public async Task<ServerDataResponse> GetAsync([FromRoute] int id)
{
// Load the node via the id
var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value);
var node = await NodeRepository
.Get()
.FirstAsync(x => x.Id == nodeId);
// Load the server with the star data attached. We filter by the node to ensure the node can only access
// servers linked to it
var server = await ServerRepository
.Get()
.Where(x => x.Node.Id == node.Id)
.Include(x => x.Star)
.ThenInclude(x => x.DockerImages)
.Include(x => x.Variables)
.Include(x => x.Allocations)
.FirstOrDefaultAsync(x => x.Id == id);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var convertedData = ConvertToServerData(server);
if (convertedData == null)
throw new HttpApiException("An error occured while creating the server data model", 500);
return convertedData;
}
[HttpGet("{id:int}/install")]
public async Task<ServerInstallDataResponse> GetInstallAsync([FromRoute] int id)
{
// Load the node via the id
var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value);
var node = await NodeRepository
.Get()
.FirstAsync(x => x.Id == nodeId);
// Load the server with the star data attached. We filter by the node to ensure the node can only access
// servers linked to it
var server = await ServerRepository
.Get()
.Where(x => x.Node.Id == node.Id)
.Include(x => x.Star)
.FirstOrDefaultAsync(x => x.Id == id);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return new ServerInstallDataResponse()
{
Script = server.Star.InstallScript,
DockerImage = server.Star.InstallDockerImage,
Shell = server.Star.InstallShell
};
}
private ServerDataResponse? ConvertToServerData(Server server)
{
// Find the docker image to use for this server
StarDockerImage? dockerImage = null;
// Handle server set image if specified
if (server.DockerImageIndex != -1)
{
dockerImage = server.Star.DockerImages
.Skip(server.DockerImageIndex)
.FirstOrDefault();
}
// Handle star default image if set
if (dockerImage == null && server.Star.DefaultDockerImage != -1)
{
dockerImage = server.Star.DockerImages
.Skip(server.Star.DefaultDockerImage)
.FirstOrDefault();
}
if (dockerImage == null)
dockerImage = server.Star.DockerImages.LastOrDefault();
if (dockerImage == null)
{
Logger.LogWarning("Unable to map server data for server {id}: No docker image available", server.Id);
return null;
}
// Convert model
return new ServerDataResponse()
{
Id = server.Id,
StartupCommand = server.StartupOverride ?? server.Star.StartupCommand,
Allocations = server.Allocations.Select(x => new AllocationDataResponse()
{
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray(),
Variables = server.Variables.ToDictionary(x => x.Key, x => x.Value),
Cpu = server.Cpu,
Disk = server.Disk,
Memory = server.Memory,
OnlineDetection = server.Star.OnlineDetection,
DockerImage = dockerImage.Identifier,
PullDockerImage = dockerImage.AutoPulling,
ParseConiguration = server.Star.ParseConfiguration,
StopCommand = server.Star.StopCommand,
};
}
}

View File

@@ -1,35 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
public class AdminAuthFilter : IServerAuthorizationFilter
{
private readonly IAuthorizationService AuthorizationService;
public int Priority => 0;
public AdminAuthFilter(IAuthorizationService authorizationService)
{
AuthorizationService = authorizationService;
}
public async Task<ServerAuthorizationResult?> ProcessAsync(
ClaimsPrincipal user,
Server server,
string permissionId,
ServerPermissionLevel requiredLevel
)
{
var authResult = await AuthorizationService.AuthorizeAsync(
user,
"permissions:admin.servers.manage"
);
return authResult.Succeeded ? ServerAuthorizationResult.Success() : null;
}
}

View File

@@ -1,34 +0,0 @@
using System.Security.Claims;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
public class OwnerAuthFilter : IServerAuthorizationFilter
{
public int Priority => 0;
public Task<ServerAuthorizationResult?> ProcessAsync(
ClaimsPrincipal user,
Server server,
string permissionId,
ServerPermissionLevel requiredLevel
)
{
var userIdValue = user.FindFirstValue("UserId");
if (string.IsNullOrEmpty(userIdValue)) // This is the case for api keys
return Task.FromResult<ServerAuthorizationResult?>(null);
var userId = int.Parse(userIdValue);
if (server.OwnerId != userId)
return Task.FromResult<ServerAuthorizationResult?>(null);
return Task.FromResult<ServerAuthorizationResult?>(
ServerAuthorizationResult.Success()
);
}
}

View File

@@ -1,56 +0,0 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
public class ShareAuthFilter : IServerAuthorizationFilter
{
private readonly DatabaseRepository<ServerShare> ShareRepository;
public ShareAuthFilter(DatabaseRepository<ServerShare> shareRepository)
{
ShareRepository = shareRepository;
}
public int Priority => 0;
public async Task<ServerAuthorizationResult?> ProcessAsync(
ClaimsPrincipal user,
Server server,
string permissionId,
ServerPermissionLevel requiredLevel
)
{
var userIdValue = user.FindFirstValue("userId");
if (string.IsNullOrEmpty(userIdValue))
return null;
var userId = int.Parse(userIdValue);
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.UserId == userId);
if (share == null)
return null;
if (string.IsNullOrEmpty(permissionId) || requiredLevel == ServerPermissionLevel.None)
return ServerAuthorizationResult.Success(share);
var possiblePermShare = share.Content.Permissions.FirstOrDefault(x => x.Identifier == permissionId);
if (possiblePermShare == null)
return null;
if (possiblePermShare.Level >= requiredLevel)
return ServerAuthorizationResult.Success(share);
return null;
}
}

View File

@@ -1,21 +0,0 @@
using System.Security.Claims;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Interfaces;
public interface IServerAuthorizationFilter
{
// Return null => skip to next filter / handler
// Return any value, instant complete
public int Priority { get; }
public Task<ServerAuthorizationResult?> ProcessAsync(
ClaimsPrincipal user,
Server server,
string permissionId,
ServerPermissionLevel requiredLevel
);
}

View File

@@ -1,18 +0,0 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.NodeAllocations;
using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class AllocationMapper
{
public static partial NodeAllocationResponse ToNodeAllocation(Allocation allocation);
public static partial Allocation ToAllocation(CreateNodeAllocationRequest request);
public static partial void Merge(UpdateNodeAllocationRequest request, Allocation allocation);
// EF Projections
public static partial IQueryable<NodeAllocationResponse> ProjectToAdminResponse(this IQueryable<Allocation> allocations);
}

View File

@@ -1,18 +0,0 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.StarDockerImages;
using MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class DockerImageMapper
{
public static partial StarDockerImageResponse ToAdminResponse(StarDockerImage dockerImage);
public static partial StarDockerImage ToDockerImage(CreateStarDockerImageRequest request);
public static partial void Merge(UpdateStarDockerImageRequest request, StarDockerImage variable);
// EF Migrations
public static partial IQueryable<StarDockerImageResponse> ProjectToAdminResponse(this IQueryable<StarDockerImage> dockerImages);
}

View File

@@ -1,18 +0,0 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.Nodes;
using MoonlightServers.Shared.Http.Responses.Admin.Nodes;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class NodeMapper
{
public static partial NodeResponse ToAdminNodeResponse(Node node);
public static partial Node ToNode(CreateNodeRequest request);
public static partial void Merge(UpdateNodeRequest request, Node node);
// EF Projections
public static partial IQueryable<NodeResponse> ProjectToAdminResponse(this IQueryable<Node> nodes);
}

View File

@@ -1,30 +0,0 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.Servers;
using MoonlightServers.Shared.Http.Responses.Admin.Servers;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class ServerMapper
{
[UserMapping(Default = true)]
public static ServerResponse ToAdminServerResponse(Server server)
{
var response = ToAdminServerResponse_Internal(server);
response.AllocationIds = server.Allocations.Select(x => x.Id).ToArray();
return response;
}
private static partial ServerResponse ToAdminServerResponse_Internal(Server server);
[MapperIgnoreSource(nameof(CreateServerRequest.Variables))]
public static partial Server ToServer(CreateServerRequest request);
public static partial void Merge(UpdateServerRequest request, Server server);
// EF Projections
public static partial IQueryable<ServerResponse> ProjectToAdminResponse(this IQueryable<Server> servers);
}

Some files were not shown because too many files have changed in this diff Show More