2 Commits

353 changed files with 3108 additions and 21520 deletions

51
.gitignore vendored
View File

@@ -301,7 +301,7 @@ node_modules/
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio 6 technical files
*.ncb
*.aps
@@ -395,44 +395,15 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
*.sln.iml
**/.idea/**
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Style builds
**/style.min.css
**/package-lock.json
**/bun.lock
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.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
# Secrets
**/.env
**/appsettings.json
**/appsettings.Development.json
**/storage

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,29 @@
<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.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<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,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5031",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7240;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.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" 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('../../../Servers.Frontend/Styles', { recursive: true });
fs.writeFileSync('../../../Servers.Frontend/Styles/Servers.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/Moonlight.Frontend/*.map";
@source "../../../Moonlight.Api/**/*.razor";
@source "../../../Moonlight.Api/**/*.cs";
@source "../../../Moonlight.Api/**/*.html";
@source "../../../Moonlight.Frontend/**/*.razor";
@source "../../../Moonlight.Frontend/**/*.cs";
@source "../../../Moonlight.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,21 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Shared.Http.Requests;
using MoonlightServers.Shared.Http.Responses;
namespace MoonlightServers.Api.Http.Controllers;
[Authorize]
[ApiController]
[Route("api/form")]
public class FormController : Controller
{
[HttpPost]
public async Task<ActionResult<FormResultDto>> PostAsync([FromBody] FormSubmitDto dto)
{
return new FormResultDto()
{
Result = dto.TextField.Replace(" ", string.Empty)
};
}
}

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moonlight.Api" Version="2.1.0">
<ExcludeAssets>content;contentfiles</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Configuration\"/>
<Folder Include="Database\Entities\"/>
<Folder Include="Database\Migrations\"/>
<Folder Include="Helpers\"/>
<Folder Include="Implementations\"/>
<Folder Include="Interfaces\"/>
<Folder Include="Mappers\"/>
<Folder Include="Models\"/>
<Folder Include="Services\"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MoonlightServers.Shared\MoonlightServers.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.Api;
using SimplePlugin.Abstractions;
using SerializationContext = MoonlightServers.Shared.SerializationContext;
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);
});
}
}

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

View File

@@ -1,15 +0,0 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Responses.Admin.ServerVariables;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class ServerVariableMapper
{
public static partial ServerVariableResponse ToAdminResponse(ServerVariable serverVariable);
// EF Projections
public static partial IQueryable<ServerVariableResponse> ProjectToAdminResponse(this IQueryable<ServerVariable> variables);
}

View File

@@ -1,27 +0,0 @@
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Enums;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper]
public static partial class ShareMapper
{
public static ServerShareContent MapToServerShareContent(Dictionary<string, ServerPermissionLevel> permissionLevels)
{
return new ServerShareContent()
{
Permissions = permissionLevels.Select(x => new ServerShareContent.SharePermission()
{
Identifier = x.Key,
Level = x.Value
}).ToList()
};
}
public static Dictionary<string, ServerPermissionLevel> MapToPermissionLevels(
ServerShareContent content)
{
return content.Permissions.ToDictionary(x => x.Identifier, x => x.Level);
}
}

View File

@@ -1,18 +0,0 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.Stars;
using MoonlightServers.Shared.Http.Responses.Admin.Stars;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class StarMapper
{
public static partial StarResponse ToAdminResponse(Star star);
public static partial Star ToStar(CreateStarRequest request);
public static partial void Merge(UpdateStarRequest request, Star star);
// EF Projections
public static partial IQueryable<StarResponse> ProjectToAdminResponse(this IQueryable<Star> stars);
}

View File

@@ -1,18 +0,0 @@
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.StarVariables;
using MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.ApiServer.Mappers;
[Mapper(AllowNullPropertyAssignment = false)]
public static partial class StarVariableMapper
{
public static partial StarVariableResponse ToAdminResponse(StarVariable variable);
public static partial StarVariable ToStarVariable(CreateStarVariableRequest request);
public static partial void Merge(UpdateStarVariableRequest request, StarVariable variable);
// EF Projections
public static partial IQueryable<StarVariableResponse> ProjectToAdminResponse(this IQueryable<StarVariable> variables);
}

View File

@@ -1,28 +0,0 @@
using MoonlightServers.ApiServer.Database.Entities;
namespace MoonlightServers.ApiServer.Models;
public record ServerAuthorizationResult
{
public bool Succeeded { get; set; }
public ServerShare? Share { get; set; }
public string? Message { get; set; }
public static ServerAuthorizationResult Success(ServerShare? share = null)
{
return new()
{
Succeeded = true,
Share = share
};
}
public static ServerAuthorizationResult Failed(string? message = null)
{
return new()
{
Succeeded = false,
Message = message
};
}
}

View File

@@ -1,14 +0,0 @@
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Models;
public record ServerShareContent
{
public List<SharePermission> Permissions { get; set; } = new();
public record SharePermission
{
public string Identifier { get; set; }
public ServerPermissionLevel Level { get; set; }
}
}

View File

@@ -1,48 +0,0 @@
namespace MoonlightServers.ApiServer.Models.Stars;
public class LegacyImageImportModel
{
public string Name { get; set; } = "";
public string Author { get; set; } = "";
public string? UpdateUrl { get; set; }
public string? DonateUrl { get; set; }
public string StartupCommand { get; set; } = "";
public string OnlineDetection { get; set; } = "";
public string StopCommand { get; set; } = "";
public string InstallShell { get; set; } = "";
public string InstallDockerImage { get; set; } = "";
public string InstallScript { get; set; } = "";
public string ParseConfiguration { get; set; } = "[]";
public int AllocationsNeeded { get; set; } = 1;
public List<ImageVariable> Variables { get; set; } = new();
public int DefaultDockerImage { get; set; } = 0;
public bool AllowDockerImageChange { get; set; } = false;
public List<ImageDockerImage> DockerImages { get; set; } = new();
public class ImageVariable
{
public string Key { get; set; } = "";
public string DefaultValue { get; set; } = "";
public string DisplayName { get; set; } = "";
public string Description { get; set; } = "";
public bool AllowView { get; set; } = false;
public bool AllowEdit { get; set; } = false;
public string? Filter { get; set; }
}
public class ImageDockerImage // Weird name xd
{
public string DisplayName { get; set; } = "";
public string Name { get; set; } = "";
public bool AutoPull { get; set; }
}
}

View File

@@ -1,8 +0,0 @@
namespace MoonlightServers.ApiServer.Models.Stars;
public class LegacyImageParseConfigModel
{
public string Type { get; set; } = "";
public string File { get; set; } = "";
public Dictionary<string, string> Configuration { get; set; } = new();
}

View File

@@ -1,55 +0,0 @@
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Models.Stars;
public class StarExportModel
{
// Meta
public string Name { get; set; }
public string Author { get; set; }
public string Version { get; set; }
public string? DonateUrl { get; set; }
public string? UpdateUrl { 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; }
// Relations
public StarVariableExportModel[] Variables { get; set; }
public StarDockerImageExportModel[] DockerImages { get; set; }
public class StarDockerImageExportModel
{
public string DisplayName { get; set; }
public string Identifier { get; set; }
public bool AutoPulling { get; set; }
}
public class StarVariableExportModel
{
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,39 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<PropertyGroup Label="Nuget Settings">
<PackageId>MoonlightServers.ApiServer</PackageId>
<Title>MoonlightServers.ApiServer</Title>
<Version>2.1.0</Version>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moonlight.ApiServer" Version="2.1.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj"/>
<ProjectReference Include="..\MoonlightServers.Shared\MoonlightServers.Shared.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Migrations\"/>
<Folder Include="Http\Middleware\"/>
<Folder Include="Startup\" />
</ItemGroup>
<ItemGroup Label="Build instruction for nuget package building">
<Compile Remove="storage\**\*"/>
<Content Remove="storage\**\*"/>
<None Remove="storage\**\*"/>
</ItemGroup>
</Project>

View File

@@ -1,59 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Extensions;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Models;
using Moonlight.ApiServer.Plugins;
using MoonlightServers.ApiServer.Database;
using MoonlightServers.ApiServer.Helpers;
using MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
using MoonlightServers.ApiServer.Interfaces;
namespace MoonlightServers.ApiServer;
public class PluginStartup : IPluginStartup
{
public void AddPlugin(WebApplicationBuilder builder)
{
// Scan the current plugin assembly for di services
builder.Services.AutoAddServices<PluginStartup>();
builder.Services.AddDbContext<ServersDataContext>();
// Configure authentication for the remote endpoints
builder.Services
.AddAuthentication()
.AddScheme<NodeAuthOptions, NodeAuthScheme>("nodeAuthentication", null);
var configuration = AppConfiguration.CreateEmpty();
builder.Configuration.Bind(configuration);
if (configuration.Frontend.EnableHosting)
{
builder.Services.AddSingleton(new FrontendConfigurationOption()
{
Scripts =
[
"/_content/MoonlightServers.Frontend/js/XtermBlazor.min.js",
"/_content/MoonlightServers.Frontend/js/addon-fit.js",
"/_content/MoonlightServers.Frontend/js/moonlightServers.js"
],
Styles = ["/_content/MoonlightServers.Frontend/css/XtermBlazor.min.css"]
});
}
// Add server auth filters
builder.Services.AddSingleton<IServerAuthorizationFilter, OwnerAuthFilter>();
builder.Services.AddScoped<IServerAuthorizationFilter, AdminAuthFilter>();
builder.Services.AddScoped<IServerAuthorizationFilter, ShareAuthFilter>();
}
public void UsePlugin(WebApplication app)
{
}
public void MapPlugin(WebApplication app)
{
}
}

View File

@@ -1,84 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Sys;
namespace MoonlightServers.ApiServer.Services;
[Singleton]
public class NodeService
{
public string CreateAccessToken(Node node, Action<Dictionary<string, object>> parameters, TimeSpan duration)
{
var claims = new Dictionary<string, object>();
parameters.Invoke(claims);
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
Expires = DateTime.UtcNow.Add(duration),
NotBefore = DateTime.UtcNow.AddSeconds(-1),
Claims = claims,
IssuedAt = DateTime.UtcNow,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
node.Token
)),
SecurityAlgorithms.HmacSha256
),
Audience = node.TokenId
};
var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor);
return jwtSecurityTokenHandler.WriteToken(securityToken);
}
public async Task<SystemStatusResponse> GetSystemStatusAsync(Node node)
{
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<SystemStatusResponse>("api/system/status");
}
#region Statistics
public async Task<StatisticsResponse> GetStatisticsAsync(Node node)
{
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsResponse>("api/statistics");
}
public async Task<StatisticsDockerResponse> GetDockerStatisticsAsync(Node node)
{
using var apiClient = CreateApiClient(node);
return await apiClient.GetJson<StatisticsDockerResponse>("api/statistics/docker");
}
#endregion
#region Helpers
public HttpApiClient CreateApiClient(Node node)
{
var url = "";
url += node.UseSsl ? "https://" : "http://";
url += $"{node.Fqdn}:{node.HttpPort}/";
var httpClient = new HttpClient()
{
BaseAddress = new Uri(url)
};
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {node.Token}");
return new HttpApiClient(httpClient);
}
#endregion
}

View File

@@ -1,44 +0,0 @@
using System.Security.Claims;
using MoonCore.Attributes;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Services;
[Scoped]
public class ServerAuthorizeService
{
private readonly IServerAuthorizationFilter[] AuthorizationFilters;
public ServerAuthorizeService(
IEnumerable<IServerAuthorizationFilter> authorizationFilters
)
{
AuthorizationFilters = authorizationFilters.ToArray();
}
public async Task<ServerAuthorizationResult> AuthorizeAsync(
ClaimsPrincipal user,
Server server,
string permissionIdentifier,
ServerPermissionLevel permissionLevel
)
{
foreach (var authorizationFilter in AuthorizationFilters)
{
var result = await authorizationFilter.ProcessAsync(
user,
server,
permissionIdentifier,
permissionLevel
);
if (result != null)
return result;
}
return ServerAuthorizationResult.Failed();
}
}

View File

@@ -1,122 +0,0 @@
using Microsoft.EntityFrameworkCore;
using MoonCore.Attributes;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.ApiServer.Services;
[Scoped]
public class ServerFileSystemService
{
private readonly NodeService NodeService;
private readonly DatabaseRepository<Server> ServerRepository;
public ServerFileSystemService(
NodeService nodeService,
DatabaseRepository<Server> serverRepository
)
{
NodeService = nodeService;
ServerRepository = serverRepository;
}
public async Task<ServerFileSystemResponse[]> ListAsync(Server server, string path)
{
using var apiClient = await GetApiClientAsync(server);
return await apiClient.GetJson<ServerFileSystemResponse[]>(
$"api/servers/{server.Id}/files/list?path={path}"
);
}
public async Task MoveAsync(Server server, string oldPath, string newPath)
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/move?oldPath={oldPath}&newPath={newPath}"
);
}
public async Task DeleteAsync(Server server, string path)
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Delete(
$"api/servers/{server.Id}/files/delete?path={path}"
);
}
public async Task MkdirAsync(Server server, string path)
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/mkdir?path={path}"
);
}
public async Task TouchAsync(Server server, string path)
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/touch?path={path}"
);
}
public async Task CompressAsync(Server server, CompressType type, string[] items, string destination)
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/compress",
new ServerFilesCompressRequest()
{
Type = type,
Items = items,
Destination = destination
}
);
}
public async Task DecompressAsync(Server server, CompressType type, string path, string destination)
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/files/decompress",
new ServerFilesDecompressRequest()
{
Type = type,
Path = path,
Destination = destination
}
);
}
#region Helpers
private async Task<HttpApiClient> GetApiClientAsync(Server server)
{
var serverWithNode = server;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
// It can be null when its not included when loading via ef !!!
if (server.Node == null)
{
serverWithNode = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstAsync(x => x.Id == server.Id);
}
return NodeService.CreateApiClient(serverWithNode.Node);
}
#endregion
}

View File

@@ -1,194 +0,0 @@
using Microsoft.EntityFrameworkCore;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
namespace MoonlightServers.ApiServer.Services;
[Scoped]
public class ServerService
{
private readonly NodeService NodeService;
private readonly DatabaseRepository<Server> ServerRepository;
public ServerService(NodeService nodeService, DatabaseRepository<Server> serverRepository)
{
NodeService = nodeService;
ServerRepository = serverRepository;
}
#region Power Actions
public async Task StartAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/start");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task StopAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/stop");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task KillAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/kill");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
#endregion
public async Task InstallAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/install");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task SyncAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post($"api/servers/{server.Id}/sync");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task SyncDeleteAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Delete($"api/servers/{server.Id}");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task<ServerStatusResponse> GetStatusAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
return await apiClient.GetJson<ServerStatusResponse>($"api/servers/{server.Id}/status");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task<ServerLogsResponse> GetLogsAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
return await apiClient.GetJson<ServerLogsResponse>($"api/servers/{server.Id}/logs");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task<ServerStatsResponse> GetStatsAsync(Server server)
{
try
{
using var apiClient = await GetApiClientAsync(server);
return await apiClient.GetJson<ServerStatsResponse>($"api/servers/{server.Id}/stats");
}
catch (HttpRequestException)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task RunCommandAsync(Server server, string command)
{
try
{
using var apiClient = await GetApiClientAsync(server);
await apiClient.Post(
$"api/servers/{server.Id}/command",
new ServerCommandRequest()
{
Command = command
}
);
}
catch (HttpRequestException)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
#region Helpers
public bool IsAllowedToAccess(User user, Server server)
{
if (server.OwnerId == user.Id)
return true;
return PermissionHelper.HasPermission(user.Permissions, "admin.servers.get");
}
private async Task<HttpApiClient> GetApiClientAsync(Server server)
{
var serverWithNode = server;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
// It can be null when its not included when loading via ef !!!
if (server.Node == null)
{
serverWithNode = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstAsync(x => x.Id == server.Id);
}
return NodeService.CreateApiClient(serverWithNode.Node);
}
#endregion
}

View File

@@ -1,410 +0,0 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Models.Stars;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Models;
namespace MoonlightServers.ApiServer.Services;
[Scoped]
public class StarImportExportService
{
private readonly DatabaseRepository<Star> StarRepository;
private readonly ILogger<StarImportExportService> Logger;
public StarImportExportService(DatabaseRepository<Star> starRepository, ILogger<StarImportExportService> logger)
{
StarRepository = starRepository;
Logger = logger;
}
public async Task<string> ExportAsync(int id)
{
var star = StarRepository
.Get()
.Include(x => x.DockerImages)
.Include(x => x.Variables)
.FirstOrDefault(x => x.Id == id);
if (star == null)
throw new HttpApiException("No star with this id found", 404);
var exportModel = new StarExportModel()
{
Name = star.Name,
Author = star.Author,
Version = star.Version,
DonateUrl = star.DonateUrl,
UpdateUrl = star.UpdateUrl,
InstallScript = star.InstallScript,
InstallShell = star.InstallShell,
InstallDockerImage = star.InstallDockerImage,
OnlineDetection = star.OnlineDetection,
StopCommand = star.StopCommand,
StartupCommand = star.StartupCommand,
ParseConfiguration = star.ParseConfiguration,
RequiredAllocations = star.RequiredAllocations,
AllowDockerImageChange = star.AllowDockerImageChange,
DefaultDockerImage = star.DefaultDockerImage,
Variables = star.Variables.Select(x => new StarExportModel.StarVariableExportModel()
{
Name = x.Name,
Type = x.Type,
Description = x.Description,
Filter = x.Filter,
Key = x.Key,
AllowEditing = x.AllowEditing,
AllowViewing = x.AllowViewing,
DefaultValue = x.DefaultValue
}).ToArray(),
DockerImages = star.DockerImages.Select(x => new StarExportModel.StarDockerImageExportModel()
{
Identifier = x.Identifier,
AutoPulling = x.AutoPulling,
DisplayName = x.DisplayName
}).ToArray()
};
var json = JsonSerializer.Serialize(exportModel, new JsonSerializerOptions()
{
WriteIndented = true
});
return json;
}
public async Task<Star> ImportAsync(string json)
{
// Determine which importer to use based on simple patterns
if (json.Contains("RequiredAllocations"))
return await ImportStarAsync(json);
else if (json.Contains("AllocationsNeeded"))
return await ImportImageAsync(json);
else if (json.Contains("_comment"))
return await ImportEggAsync(json);
else
throw new HttpApiException("Unable to determine the format of the imported star/image/egg", 400);
}
public async Task<Star> ImportStarAsync(string json)
{
try
{
var model = JsonSerializer.Deserialize<StarExportModel>(json, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
ArgumentNullException.ThrowIfNull(model);
var star = new Star()
{
Name = model.Name,
Author = model.Author,
Version = model.Version,
DonateUrl = model.DonateUrl,
UpdateUrl = model.UpdateUrl,
InstallScript = model.InstallScript,
InstallShell = model.InstallShell,
InstallDockerImage = model.InstallDockerImage,
OnlineDetection = model.OnlineDetection,
StopCommand = model.StopCommand,
StartupCommand = model.StartupCommand,
ParseConfiguration = model.ParseConfiguration,
RequiredAllocations = model.RequiredAllocations,
AllowDockerImageChange = model.AllowDockerImageChange,
DefaultDockerImage = model.DefaultDockerImage,
Variables = model.Variables.Select(x => new StarVariable()
{
DefaultValue = x.DefaultValue,
Description = x.Description,
Filter = x.Filter,
Key = x.Key,
AllowEditing = x.AllowEditing,
AllowViewing = x.AllowViewing,
Type = x.Type,
Name = x.Name
}).ToList(),
DockerImages = model.DockerImages.Select(x => new StarDockerImage()
{
DisplayName = x.DisplayName,
AutoPulling = x.AutoPulling,
Identifier = x.Identifier
}).ToList()
};
var finalStar = await StarRepository.AddAsync(star);
return finalStar;
}
catch (Exception e)
{
Logger.LogError("An unhandled error occured while importing star: {e}", e);
throw new HttpApiException("An unhandled error occured while importing star", 400);
}
}
public async Task<Star> ImportImageAsync(string json)
{
try
{
var model = JsonSerializer.Deserialize<LegacyImageImportModel>(json, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
ArgumentNullException.ThrowIfNull(model);
var star = new Star()
{
Name = model.Name,
Author = model.Author,
Version = "Imported from v2.0",
DonateUrl = model.DonateUrl,
UpdateUrl = model.UpdateUrl,
InstallScript = model.InstallScript,
InstallShell = model.InstallShell,
InstallDockerImage = model.InstallDockerImage,
OnlineDetection = model.OnlineDetection,
StopCommand = model.StopCommand,
StartupCommand = model.StartupCommand,
RequiredAllocations = model.AllocationsNeeded,
AllowDockerImageChange = model.AllowDockerImageChange,
DefaultDockerImage = model.DefaultDockerImage,
Variables = model.Variables.Select(x => new StarVariable()
{
DefaultValue = x.DefaultValue,
Description = x.Description,
Filter = x.Filter,
Key = x.Key,
AllowEditing = x.AllowEdit,
AllowViewing = x.AllowView,
Type = StarVariableType.Text,
Name = x.DisplayName
}).ToList(),
DockerImages = model.DockerImages.Select(x => new StarDockerImage()
{
DisplayName = x.DisplayName,
AutoPulling = x.AutoPull,
Identifier = x.Name
}).ToList()
};
#region Convert parse configurations
var oldParseConfig = JsonSerializer.Deserialize<LegacyImageParseConfigModel[]>(model.ParseConfiguration,
new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
}
);
ArgumentNullException.ThrowIfNull(oldParseConfig);
var newParseConfig = new List<ParseConfiguration>();
// Remap values
foreach (var config in oldParseConfig)
{
var parseConfiguration = new ParseConfiguration()
{
File = config.File,
Parser = Enum.TryParse(config.Type, true, out FileParsers parserType)
? parserType
: FileParsers.File
};
foreach (var option in config.Configuration)
{
parseConfiguration.Entries.Add(new ParseConfiguration.ParseConfigurationEntry()
{
Key = option.Key,
Value = option.Value
});
}
newParseConfig.Add(parseConfiguration);
}
star.ParseConfiguration = JsonSerializer.Serialize(newParseConfig);
#endregion
var finalStar = await StarRepository.AddAsync(star);
return finalStar;
}
catch (Exception e)
{
Logger.LogError("An unhandled error occured while importing image: {e}", e);
throw new HttpApiException("An unhandled error occured while importing image", 400);
}
}
public async Task<Star> ImportEggAsync(string json)
{
// Create result
var star = new Star();
// Prepare json
var fixedJson = json;
fixedJson = fixedJson.Replace("\\/", "/");
var jsonDocument = JsonDocument.Parse(fixedJson);
var egg = jsonDocument.RootElement;
// Let's go :O
// Basic meta
star.Name = egg.GetProperty("name").GetString() ?? "Parse error";
star.Author = egg.GetProperty("author").GetString() ?? "Parse error";
// Start & Stop and Status
var configSection = egg.GetProperty("config");
star.StartupCommand = egg.GetProperty("startup").GetString() ?? "Parse error";
star.StopCommand = configSection.GetProperty("stop").GetString() ?? "Parse error";
var startupSectionJson = configSection.GetProperty("startup").GetString() ?? "{}";
var startupSectionDocument = JsonDocument.Parse(startupSectionJson);
var startupSection = startupSectionDocument.RootElement;
var doneProperty = startupSection.GetProperty("done");
if (doneProperty.ValueKind == JsonValueKind.Array)
star.OnlineDetection = doneProperty.Deserialize<string[]>()?.First() ?? "Parse error";
else
star.OnlineDetection = doneProperty.GetString() ?? "Parse error";
// Installation
var installationSection = egg.GetProperty("scripts").GetProperty("installation");
var shell = installationSection.GetProperty("entrypoint").GetString() ?? "bash";
star.InstallShell = shell.StartsWith("/") ? shell : $"/bin/{shell}";
star.InstallDockerImage = installationSection.GetProperty("container").GetString() ?? "Parse error";
star.InstallScript = installationSection.GetProperty("script").GetString() ?? "Parse error";
// Variables
var variables = egg.GetProperty("variables");
foreach (var variable in variables.EnumerateArray())
{
var starVariable = new StarVariable()
{
Name = variable.GetProperty("name").GetString() ?? "Parse error",
Description = variable.GetProperty("description").GetString() ?? "Parse error",
Key = variable.GetProperty("env_variable").GetString() ?? "Parse error",
DefaultValue = variable.GetProperty("default_value").GetString() ?? "Parse error",
Type = StarVariableType.Text
};
// Check if the provided value is an int or a boolean as both are apparently valid
if (variable.GetProperty("user_editable").ValueKind == JsonValueKind.Number)
{
starVariable.AllowEditing = variable.GetProperty("user_editable").GetInt32() == 1;
starVariable.AllowViewing = variable.GetProperty("user_viewable").GetInt32() == 1;
}
else
{
starVariable.AllowEditing = variable.GetProperty("user_editable").GetBoolean();
starVariable.AllowViewing = variable.GetProperty("user_viewable").GetBoolean();
}
star.Variables.Add(starVariable);
}
// Docker images
if (egg.TryGetProperty("image", out var imageProperty)) // Variant 1
{
star.DockerImages.Add(new StarDockerImage()
{
Identifier = imageProperty.GetString() ?? "Parse error",
DisplayName = imageProperty.GetString() ?? "Parse error",
AutoPulling = true
});
}
else if (egg.TryGetProperty("images", out var imagesProperty)) // Variant 2
{
foreach (var di in imagesProperty.EnumerateObject())
{
star.DockerImages.Add(new StarDockerImage()
{
DisplayName = di.Name,
Identifier = di.Value.GetString() ?? "Parse error",
AutoPulling = true
});
}
}
else if (egg.TryGetProperty("docker_images", out var dockerImages)) // Variant 3
{
foreach (var di in dockerImages.EnumerateObject())
{
star.DockerImages.Add(new StarDockerImage()
{
DisplayName = di.Name,
Identifier = di.Value.GetString() ?? "Parse error",
AutoPulling = true
});
}
}
// Parse configuration
var parseConfigurationJson = configSection.GetProperty("files").GetString() ?? "{}";
var parseConfigurationDocument = JsonDocument.Parse(parseConfigurationJson);
var parseConfiguration = parseConfigurationDocument.RootElement;
var resultPcs = new List<ParseConfiguration>();
foreach (var pConfig in parseConfiguration.EnumerateObject())
{
var pc = new ParseConfiguration()
{
File = pConfig.Name
};
var parser = pConfig.Value.GetProperty("parser").GetString() ?? "Parse error";
pc.Parser = Enum.TryParse(parser, true, out FileParsers fileParser) ? fileParser : FileParsers.File;
foreach (var pConfigFind in pConfig.Value.GetProperty("find").EnumerateObject())
{
var entry = new ParseConfiguration.ParseConfigurationEntry()
{
Key = pConfigFind.Name,
Value = pConfigFind.Value.GetString() ?? "Parse error"
};
// Fix up special variables
entry.Value = entry.Value.Replace("server.allocations.default.port", "SERVER_PORT");
entry.Value = entry.Value.Replace("server.build.default.port", "SERVER_PORT");
pc.Entries.Add(entry);
}
resultPcs.Add(pc);
}
star.ParseConfiguration = JsonSerializer.Serialize(resultPcs);
// Post parse fixes
// - Stop command signal
// Some weird eggs use ^^C in as a stop command, so we need to handle this as well
// because moonlight handles power signals correctly, wings does/did not
star.StopCommand = star.StopCommand.Replace("^^C", "^C");
// - Set moonlight native values
star.RequiredAllocations = 1;
star.Version = "Imported egg";
star.AllowDockerImageChange = true;
// Finally save it to the db
var finalStar = await StarRepository.AddAsync(star);
return finalStar;
}
}

View File

@@ -1,72 +0,0 @@
namespace MoonlightServers.Daemon.Configuration;
public class AppConfiguration
{
public DockerData Docker { get; set; } = new();
public StorageData Storage { get; set; } = new();
public SecurityData Security { get; set; } = new();
public RemoteData Remote { get; set; } = new();
public FilesData Files { get; set; } = new();
public ServerData Server { get; set; } = new();
public KestrelData Kestrel { get; set; } = new();
public class KestrelData
{
public int RequestBodySizeLimit { get; set; } = 25;
}
public class RemoteData
{
public string Url { get; set; }
}
public class DockerData
{
public string Uri { get; set; } = "unix:///var/run/docker.sock";
public DockerCredentialData[] Credentials { get; set; } = [];
public class DockerCredentialData
{
public string Domain { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Email { get; set; }
}
}
public class SecurityData
{
public string Token { get; set; }
public string TokenId { get; set; }
}
public class StorageData
{
public string Volumes { get; set; } = Path.Combine("storage", "volumes");
public string VirtualDisks { get; set; } = Path.Combine("storage", "virtualDisks");
public string Backups { get; set; } = Path.Combine("storage", "backups");
public string Install { get; set; } =Path.Combine("storage", "install");
public VirtualDiskData VirtualDiskOptions { get; set; } = new();
}
public record VirtualDiskData
{
public string FileSystemType { get; set; } = "ext4";
public string E2FsckParameters { get; set; } = "-pf";
}
public class FilesData
{
public int UploadSizeLimit { get; set; } = 1024 * 2;
public int UploadChunkSize { get; set; } = 20;
}
public class ServerData
{
public int WaitBeforeKillSeconds { get; set; } = 30;
public int TmpFsSize { get; set; } = 100;
public float MemoryOverheadMultiplier { get; set; } = 0.05f;
public int ConsoleMessageCacheLimit { get; set; } = 250;
}
}

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Daemon.Configuration;
public class DockerOptions
{
public string SocketUri { get; set; } = "unix:///var/run/docker.sock";
}

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Daemon.Configuration;
public class LocalStorageOptions
{
public string InstallPath { get; set; } = "/data/install";
public string RuntimePath { get; set; } = "/data/runtime";
}

View File

@@ -1,10 +0,0 @@
namespace MoonlightServers.Daemon.Enums;
public enum ServerState
{
Offline = 0,
Starting = 1,
Online = 2,
Stopping = 3,
Installing = 4
}

View File

@@ -1,14 +0,0 @@
namespace MoonlightServers.Daemon.Enums;
public enum ServerTrigger
{
Start = 0,
Stop = 1,
Restart = 2,
Kill = 3,
Reinstall = 4,
NotifyOnline = 5,
NotifyRuntimeContainerDied = 6,
NotifyInstallationContainerDied = 7,
NotifyInternalError = 8
}

View File

@@ -1,359 +0,0 @@
using Docker.DotNet.Models;
using Mono.Unix.Native;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Extensions;
public static class ServerConfigurationExtensions
{
public static ServerConfiguration ToServerConfiguration(this ServerDataResponse response)
{
return new ServerConfiguration()
{
Id = response.Id,
StartupCommand = response.StartupCommand,
Allocations = response.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
{
IpAddress = y.IpAddress,
Port = y.Port
}).ToArray(),
Variables = response.Variables,
OnlineDetection = response.OnlineDetection,
DockerImage = response.DockerImage,
Cpu = response.Cpu,
Disk = response.Disk,
Memory = response.Memory,
StopCommand = response.StopCommand
};
}
public static CreateContainerParameters ToRuntimeCreateParameters(
this ServerConfiguration configuration,
AppConfiguration appConfiguration,
string hostPath,
string containerName
)
{
var parameters = configuration.ToSharedCreateParameters(appConfiguration);
#region Security
parameters.HostConfig.CapDrop = new List<string>()
{
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap"
};
parameters.HostConfig.ReadonlyRootfs = true;
parameters.HostConfig.SecurityOpt = new List<string>()
{
"no-new-privileges"
};
#endregion
#region Name
parameters.Name = containerName;
parameters.Hostname = containerName;
#endregion
#region Docker Image
parameters.Image = configuration.DockerImage;
#endregion
#region Environment
parameters.Env = configuration.ToEnvironmentVariables()
.Select(x => $"{x.Key}={x.Value}")
.ToList();
#endregion
#region Working Dir
parameters.WorkingDir = "/home/container";
#endregion
#region User
var userId = Syscall.getuid(); // TODO: Extract to external service?
if (userId == 0)
userId = 998;
parameters.User = $"{userId}:{userId}";
/*
if (userId == 0)
{
// We are running as root, so we need to run the container as another user and chown the files when we make changes
parameters.User = $"998:998";
}
else
{
// We are not running as root, so we start the container as the same user,
// as we are not able to chown the container content to a different user
parameters.User = $"{userId}:{userId}";
}*/
#endregion
#region Mounts
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new()
{
Source = hostPath,
Target = "/home/container",
ReadOnly = false,
Type = "bind"
});
#endregion
#region Port Bindings
if (true) // TODO: Add network toggle
{
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
foreach (var allocation in configuration.Allocations)
{
parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new());
parameters.ExposedPorts.Add($"{allocation.Port}/udp", new());
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/tcp", new List<PortBinding>
{
new()
{
HostPort = allocation.Port.ToString(),
HostIP = allocation.IpAddress
}
});
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/udp", new List<PortBinding>
{
new()
{
HostPort = allocation.Port.ToString(),
HostIP = allocation.IpAddress
}
});
}
}
#endregion
return parameters;
}
public static CreateContainerParameters ToInstallationCreateParameters(
this ServerConfiguration configuration,
AppConfiguration appConfiguration,
string runtimeHostPath,
string installationHostPath,
string containerName,
string installDockerImage,
string installShell
)
{
var parameters = configuration.ToSharedCreateParameters(appConfiguration);
// - Name
parameters.Name = containerName;
parameters.Hostname = containerName;
// - Image
parameters.Image = installDockerImage;
// - Env
parameters.Env = configuration
.ToEnvironmentVariables()
.Select(x => $"{x.Key}={x.Value}")
.ToList();
// -- Working directory
parameters.WorkingDir = "/mnt/server";
// - User
// Note: Some images might not work if we set a user here
var userId = Syscall.getuid();
// If we are root, we are able to change owner permissions after the installation
// so we run the installation as root, otherwise we need to run it as our current user,
// so we are able to access the files created by the installer
if (userId == 0)
parameters.User = "0:0";
else
parameters.User = $"{userId}:{userId}";
// -- Mounts
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new()
{
Source = runtimeHostPath,
Target = "/mnt/server",
ReadOnly = false,
Type = "bind"
});
parameters.HostConfig.Mounts.Add(new()
{
Source = installationHostPath,
Target = "/mnt/install",
ReadOnly = false,
Type = "bind"
});
parameters.Cmd = [installShell, "/mnt/install/install.sh"];
return parameters;
}
private static CreateContainerParameters ToSharedCreateParameters(this ServerConfiguration configuration,
AppConfiguration appConfiguration)
{
var parameters = new CreateContainerParameters()
{
HostConfig = new()
};
#region Input, output & error streams and tty
parameters.Tty = true;
parameters.AttachStderr = true;
parameters.AttachStdin = true;
parameters.AttachStdout = true;
parameters.OpenStdin = true;
#endregion
#region CPU
parameters.HostConfig.CPUQuota = configuration.Cpu * 1000;
parameters.HostConfig.CPUPeriod = 100000;
parameters.HostConfig.CPUShares = 1024;
#endregion
#region Memory & Swap
var memoryLimit = configuration.Memory;
// The overhead multiplier gives the container a little bit more memory to prevent crashes
var memoryOverhead = memoryLimit + (memoryLimit * appConfiguration.Server.MemoryOverheadMultiplier);
long swapLimit = -1;
/*
// If swap is enabled globally and not disabled on this server, set swap
if (!configuration.Limits.DisableSwap && config.Server.EnableSwap)
swapLimit = (long)(memoryOverhead + memoryOverhead * config.Server.SwapMultiplier);
co
*/
// Finalize limits by converting and updating the host config
parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes;
parameters.HostConfig.MemoryReservation = ByteConverter.FromMegaBytes(memoryLimit, 1000).Bytes;
parameters.HostConfig.MemorySwap =
swapLimit == -1 ? swapLimit : ByteConverter.FromMegaBytes(swapLimit, 1000).Bytes;
#endregion
#region Misc Limits
// -- Other limits
parameters.HostConfig.BlkioWeight = 100;
//container.HostConfig.PidsLimit = configuration.Limits.PidsLimit;
parameters.HostConfig.OomKillDisable = true; //!configuration.Limits.EnableOomKill;
#endregion
#region DNS
// TODO: Read hosts dns settings?
parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List<string>()
{
"1.1.1.1",
"9.9.9.9"
};
#endregion
#region Tmpfs
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
{
{ "/tmp", $"rw,exec,nosuid,size={appConfiguration.Server.TmpFsSize}M" }
};
#endregion
#region Logging
parameters.HostConfig.LogConfig = new()
{
Type = "json-file", // We need to use this provider, as the GetLogs endpoint needs it
Config = new Dictionary<string, string>()
};
#endregion
#region Labels
parameters.Labels = new Dictionary<string, string>();
parameters.Labels.Add("Software", "Moonlight-Panel");
parameters.Labels.Add("ServerId", configuration.Id.ToString());
#endregion
return parameters;
}
public static Dictionary<string, string> ToEnvironmentVariables(this ServerConfiguration configuration)
{
var result = new Dictionary<string, string>
{
//TODO: Add timezone, add server ip
{ "STARTUP", configuration.StartupCommand },
{ "SERVER_MEMORY", configuration.Memory.ToString() }
};
if (configuration.Allocations.Length > 0)
{
var mainAllocation = configuration.Allocations.First();
result.Add("SERVER_IP", mainAllocation.IpAddress);
result.Add("SERVER_PORT", mainAllocation.Port.ToString());
}
// Handle allocation variables
var i = 1;
foreach (var allocation in configuration.Allocations)
{
result.Add($"ML_PORT_{i}", allocation.Port.ToString());
i++;
}
// Copy variables as env vars
foreach (var variable in configuration.Variables)
result.Add(variable.Key, variable.Value);
return result;
}
}

View File

@@ -1,66 +0,0 @@
using Stateless;
namespace MoonlightServers.Daemon.Extensions;
public static class StateConfigurationExtensions
{
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFrom<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Action entryAction
)
{
configuration.OnExit(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return;
entryAction.Invoke();
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFrom<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Action<StateMachine<TState, TTrigger>.Transition> entryAction
)
{
configuration.OnExit(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return;
entryAction.Invoke(transition);
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFromAsync<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Func<Task> entryAction
)
{
configuration.OnExitAsync(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return Task.CompletedTask;
return entryAction.Invoke();
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFromAsync<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Func<StateMachine<TState, TTrigger>.Transition, Task> entryAction
)
{
configuration.OnExitAsync(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return Task.CompletedTask;
return entryAction.Invoke(transition);
});
return configuration;
}
}

View File

@@ -0,0 +1,76 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace MoonlightServers.Daemon.Helpers;
public class AppConsoleFormatter : ConsoleFormatter
{
private const string TimestampColor = "\e[38;2;148;148;148m";
private const string CategoryColor = "\e[38;2;198;198;198m";
private const string MessageColor = "\e[38;2;255;255;255m";
private const string Bold = "\e[1m";
// Pre-computed ANSI color codes for each log level
private const string CriticalColor = "\e[38;2;255;0;0m";
private const string ErrorColor = "\e[38;2;255;0;0m";
private const string WarningColor = "\e[38;2;215;215;0m";
private const string InfoColor = "\e[38;2;135;215;255m";
private const string DebugColor = "\e[38;2;198;198;198m";
private const string TraceColor = "\e[38;2;68;68;68m";
public AppConsoleFormatter() : base(nameof(AppConsoleFormatter))
{
}
public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider? scopeProvider,
TextWriter textWriter)
{
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
// Timestamp
textWriter.Write(TimestampColor);
textWriter.Write(DateTime.Now.ToString("dd.MM.yy HH:mm:ss"));
textWriter.Write(' ');
// Log level with color and bold
var (levelText, levelColor) = GetLevelInfo(logEntry.LogLevel);
textWriter.Write(levelColor);
textWriter.Write(Bold);
textWriter.Write(levelText);
textWriter.Write(' ');
// Category
textWriter.Write(CategoryColor);
textWriter.Write(logEntry.Category);
// Message
textWriter.Write(MessageColor);
textWriter.Write(": ");
textWriter.Write(message);
// Exception
if (logEntry.Exception != null)
{
textWriter.Write(MessageColor);
textWriter.WriteLine(logEntry.Exception.ToString());
}
else
textWriter.WriteLine();
}
private static (string text, string color) GetLevelInfo(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Critical => ("CRIT", CriticalColor),
LogLevel.Error => ("ERRO", ErrorColor),
LogLevel.Warning => ("WARN", WarningColor),
LogLevel.Information => ("INFO", InfoColor),
LogLevel.Debug => ("DEBG", DebugColor),
LogLevel.Trace => ("TRCE", TraceColor),
_ => ("NONE", "")
};
}
}

View File

@@ -1,31 +0,0 @@
namespace MoonlightServers.Daemon.Helpers;
public class CompositeServiceProvider : IServiceProvider
{
private readonly List<IServiceProvider> ServiceProviders;
public CompositeServiceProvider(params IServiceProvider[] serviceProviders)
{
ServiceProviders = new List<IServiceProvider>(serviceProviders);
}
public object? GetService(Type serviceType)
{
foreach (var provider in ServiceProviders)
{
try
{
var service = provider.GetService(serviceType);
if (service != null)
return service;
}
catch (InvalidOperationException)
{
// Ignored
}
}
return null;
}
}

View File

@@ -1,263 +0,0 @@
using System.Runtime.InteropServices;
using Mono.Unix.Native;
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.Helpers;
[Singleton]
public class HostSystemHelper
{
private readonly ILogger<HostSystemHelper> Logger;
public HostSystemHelper(ILogger<HostSystemHelper> logger)
{
Logger = logger;
}
public string GetOsName()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Windows platform detected
var osVersion = Environment.OSVersion.Version;
return $"Windows {osVersion.Major}.{osVersion.Minor}.{osVersion.Build}";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var releaseRaw = File
.ReadAllLines("/etc/os-release")
.FirstOrDefault(x => x.StartsWith("PRETTY_NAME="));
if (string.IsNullOrEmpty(releaseRaw))
return "Linux (unknown release)";
var release = releaseRaw
.Replace("PRETTY_NAME=", "")
.Replace("\"", "");
if (string.IsNullOrEmpty(release))
return "Linux (unknown release)";
return release;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// macOS platform detected
var osVersion = Environment.OSVersion.Version;
return $"Shitty macOS {osVersion.Major}.{osVersion.Minor}.{osVersion.Build}";
}
// Unknown platform
return "Unknown";
}
#region CPU Usage
public async Task<CpuUsageDetails> GetCpuUsageAsync()
{
var result = new CpuUsageDetails();
var perCoreUsages = new List<double>();
// Initial read
var (cpuLastStats, cpuLastSums) = await ReadAllCpuStatsAsync();
await Task.Delay(1000);
// Second read
var (cpuNowStats, cpuNowSums) = await ReadAllCpuStatsAsync();
for (var i = 0; i < cpuNowStats.Length; i++)
{
var cpuDelta = cpuNowSums[i] - cpuLastSums[i];
var cpuIdle = cpuNowStats[i][3] - cpuLastStats[i][3];
var cpuUsed = cpuDelta - cpuIdle;
var usage = 100.0 * cpuUsed / cpuDelta;
if (i == 0)
result.OverallUsage = usage;
else
perCoreUsages.Add(usage);
}
result.PerCoreUsage = perCoreUsages.ToArray();
// Get model name
var cpuInfoLines = await File.ReadAllLinesAsync("/proc/cpuinfo");
var modelLine = cpuInfoLines.FirstOrDefault(x => x.StartsWith("model name"));
result.Model = modelLine?.Split(":")[1].Trim() ?? "N/A";
return result;
}
private async Task<(long[][] cpuStatsList, long[] cpuSums)> ReadAllCpuStatsAsync()
{
var lines = await File.ReadAllLinesAsync("/proc/stat");
lines = lines.Where(line => line.StartsWith("cpu"))
.TakeWhile(line => line.StartsWith("cpu")) // Ensures only CPU lines are read
.ToArray();
var statsList = new List<long[]>();
var sumList = new List<long>();
foreach (var line in lines)
{
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Skip(1) // Skip the "cpu" label
.ToArray();
var cpuTimes = parts
.Select(long.Parse)
.ToArray();
var sum = cpuTimes.Sum();
statsList.Add(cpuTimes);
sumList.Add(sum);
}
return (statsList.ToArray(), sumList.ToArray());
}
#endregion
#region Memory
public async Task ClearCachedMemoryAsync()
{
await File.WriteAllTextAsync("/proc/sys/vm/drop_caches", "3");
}
public async Task<MemoryUsageDetails> GetMemoryUsageAsync()
{
var details = new MemoryUsageDetails();
var lines = await File.ReadAllLinesAsync("/proc/meminfo");
foreach (var line in lines)
{
// We want to ignore all non kilobyte values
if (!line.Contains("kB"))
continue;
// Split the line up so we can extract the id and the value
// to map it to the model field
var parts = line.Split(":");
var id = parts[0];
var value = parts[1]
.Replace("kB", "")
.Trim();
if (!long.TryParse(value, out var longValue))
continue;
var bytes = ByteConverter.FromKiloBytes(longValue).Bytes;
switch (id)
{
case "MemTotal":
details.Total = bytes;
break;
case "MemFree":
details.Free = bytes;
break;
case "MemAvailable":
details.Available = bytes;
break;
case "Cached":
details.Cached = bytes;
break;
case "SwapTotal":
details.SwapTotal = bytes;
break;
case "SwapFree":
details.SwapFree = bytes;
break;
}
}
return details;
}
#endregion
#region Disks
public async Task<DiskUsageDetails[]> GetDiskUsagesAsync()
{
var details = new List<DiskUsageDetails>();
// First we need to check which mounts actually exist
var diskDevices = new Dictionary<string, string>();
string[] ignoredMounts = ["/boot/efi", "/boot"];
var mountLines = await File.ReadAllLinesAsync("/proc/mounts");
foreach (var mountLine in mountLines)
{
var parts = mountLine.Split(" ");
var device = parts[0];
var mountedAt = parts[1];
// We only want to handle mounted physical devices
if (!device.StartsWith("/dev/"))
continue;
// Ignore certain mounts which we dont want to show
if (ignoredMounts.Contains(mountedAt))
continue;
diskDevices.Add(device, mountedAt);
}
foreach (var diskMount in diskDevices)
{
var device = diskMount.Key;
var mount = diskMount.Value;
var statusCode = Syscall.statvfs(mount, out var statvfs);
if (statusCode != 0)
{
var error = Stdlib.GetLastError();
Logger.LogError(
"An error occured while checking disk stats for mount {mount}: {error}",
mount,
error
);
continue;
}
// Source: https://man7.org/linux/man-pages/man3/statvfs.3.html
var detail = new DiskUsageDetails()
{
Device = device,
MountPath = mount,
DiskTotal = statvfs.f_blocks * statvfs.f_frsize,
DiskFree = statvfs.f_bfree * statvfs.f_frsize,
InodesTotal = statvfs.f_files,
InodesFree = statvfs.f_ffree
};
details.Add(detail);
}
return details.ToArray();
}
#endregion
}

View File

@@ -1,354 +0,0 @@
using System.Text;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
using Mono.Unix.Native;
using MoonCore.Unix.SecureFs;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Helpers;
public class ServerFileSystem
{
private readonly SecureFileSystem FileSystem;
public ServerFileSystem(SecureFileSystem fileSystem)
{
FileSystem = fileSystem;
}
public Task<ServerFileSystemResponse[]> ListAsync(string inputPath)
{
var path = Normalize(inputPath);
var entries = FileSystem.ReadDir(path);
IEnumerable<SecureFsEntry> entryQuery = entries;
// Filter all lost+found directories on the root of the file system
// to hide the folder shown by virtual disk volumes
if (string.IsNullOrEmpty(inputPath) || inputPath == "/")
entryQuery = entryQuery.Where(x => x.Name != "lost+found");
var result = entryQuery
.Select(x => new ServerFileSystemResponse()
{
Name = x.Name,
IsFolder = x.IsDirectory,
Size = x.Size,
UpdatedAt = x.LastChanged,
CreatedAt = x.CreatedAt
})
.ToArray();
return Task.FromResult(result);
}
public Task MoveAsync(string inputOldPath, string inputNewPath)
{
var oldPath = Normalize(inputOldPath);
var newPath = Normalize(inputNewPath);
FileSystem.Rename(oldPath, newPath);
return Task.CompletedTask;
}
public Task DeleteAsync(string inputPath)
{
var path = Normalize(inputPath);
FileSystem.RemoveAll(path);
return Task.CompletedTask;
}
public Task MkdirAsync(string inputPath)
{
var path = Normalize(inputPath);
FileSystem.MkdirAll(path, FilePermissions.ACCESSPERMS);
return Task.CompletedTask;
}
public Task TouchAsync(string inputPath)
{
var path = Normalize(inputPath);
var parentDirectory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(
path,
_ => { },
OpenFlags.O_CREAT
); // We use these custom flags to ensure we aren't overwriting the file
return Task.CompletedTask;
}
public Task CreateChunkAsync(string inputPath, long totalSize, long positionToSkip, Stream chunkStream)
{
var path = Normalize(inputPath);
var parentDirectory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(path, fileStream =>
{
if (fileStream.Length != totalSize)
fileStream.SetLength(totalSize);
fileStream.Position = positionToSkip;
chunkStream.CopyTo(fileStream);
fileStream.Flush();
}, OpenFlags.O_CREAT | OpenFlags.O_RDWR); // We use these custom flags to ensure we aren't overwriting the file
return Task.CompletedTask;
}
public Task CreateAsync(string inputPath, Stream dataStream)
{
var path = Normalize(inputPath);
var parentDirectory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(path, stream =>
{
stream.Position = 0;
dataStream.CopyTo(stream);
stream.Flush();
});
return Task.CompletedTask;
}
public Task ReadAsync(string inputPath, Func<Stream, Task> onHandle)
{
var path = Normalize(inputPath);
FileSystem.OpenFileRead(path, stream =>
{
// No try catch here because the safe fs abstraction already handles every error occuring in the handle
onHandle.Invoke(stream).Wait();
});
return Task.CompletedTask;
}
#region Compression
public Task CompressAsync(string[] itemsInput, string destinationInput, CompressType type)
{
var destination = Normalize(destinationInput);
var items = itemsInput.Select(Normalize);
if (type == CompressType.Zip)
{
FileSystem.OpenFileWrite(destination, stream =>
{
using var zipStream = new ZipOutputStream(stream);
foreach (var item in items)
AddItemToZip(item, zipStream);
zipStream.Flush();
stream.Flush();
zipStream.Close();
});
}
else if (type == CompressType.TarGz)
{
FileSystem.OpenFileWrite(destination, stream =>
{
using var gzStream = new GZipOutputStream(stream);
using var tarStream = new TarOutputStream(gzStream, Encoding.UTF8);
foreach (var item in items)
AddItemToTar(item, tarStream);
tarStream.Flush();
gzStream.Flush();
stream.Flush();
tarStream.Close();
gzStream.Close();
});
}
return Task.CompletedTask;
}
public Task DecompressAsync(string pathInput, string destinationInput, CompressType type)
{
var path = Normalize(pathInput);
var destination = Normalize(destinationInput);
if (type == CompressType.Zip)
{
FileSystem.OpenFileRead(path, fileStream =>
{
var zipInputStream = new ZipInputStream(fileStream);
ExtractZip(zipInputStream, destination);
});
}
else if (type == CompressType.TarGz)
{
FileSystem.OpenFileRead(path, fileStream =>
{
var gzInputStream = new GZipInputStream(fileStream);
var zipInputStream = new TarInputStream(gzInputStream, Encoding.UTF8);
ExtractTar(zipInputStream, destination);
});
}
return Task.CompletedTask;
}
private void AddItemToZip(string path, ZipOutputStream outputStream)
{
var item = FileSystem.Stat(path);
if (item.IsDirectory)
{
var contents = FileSystem.ReadDir(path);
foreach (var content in contents)
{
AddItemToZip(
Path.Combine(path, content.Name),
outputStream
);
}
}
else
{
var entry = new ZipEntry(path)
{
Size = item.Size,
DateTime = item.LastChanged
};
outputStream.PutNextEntry(entry);
FileSystem.OpenFileRead(path, stream => { stream.CopyTo(outputStream); });
outputStream.CloseEntry();
}
}
private void AddItemToTar(string path, TarOutputStream outputStream)
{
var item = FileSystem.Stat(path);
if (item.IsDirectory)
{
var contents = FileSystem.ReadDir(path);
foreach (var content in contents)
{
AddItemToTar(
Path.Combine(path, content.Name),
outputStream
);
}
}
else
{
var entry = TarEntry.CreateTarEntry(path);
entry.Name = path;
entry.Size = item.Size;
entry.ModTime = item.LastChanged;
outputStream.PutNextEntry(entry);
FileSystem.OpenFileRead(path, stream => { stream.CopyTo(outputStream); });
outputStream.CloseEntry();
}
}
private void ExtractZip(ZipInputStream inputStream, string destination)
{
while (true)
{
var entry = inputStream.GetNextEntry();
if (entry == null)
break;
if (entry.IsDirectory)
continue;
var fileDestination = Path.Combine(destination, entry.Name);
var parentDirectory = Path.GetDirectoryName(fileDestination);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(fileDestination, stream =>
{
stream.Position = 0;
inputStream.CopyTo(stream);
stream.Flush();
}); // This will override the file if it exists
}
}
private void ExtractTar(TarInputStream inputStream, string destination)
{
while (true)
{
var entry = inputStream.GetNextEntry();
if (entry == null)
break;
if (entry.IsDirectory)
continue;
var fileDestination = Path.Combine(destination, entry.Name);
var parentDirectory = Path.GetDirectoryName(fileDestination);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(fileDestination, stream =>
{
stream.Position = 0;
inputStream.CopyTo(stream);
stream.Flush();
}); // This will override the file if it exists
}
}
#endregion
private string Normalize(string path)
{
return path
.Replace("//", "/")
.Replace("..", "")
.TrimStart('/');
}
}

View File

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

View File

@@ -1,49 +0,0 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
namespace MoonlightServers.Daemon.Helpers;
public class TokenAuthScheme : AuthenticationHandler<TokenAuthOptions>
{
public TokenAuthScheme(IOptionsMonitor<TokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
public TokenAuthScheme(IOptionsMonitor<TokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(
options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
return Task.FromResult(AuthenticateResult.NoResult());
var authHeaderValue = Request.Headers["Authorization"].FirstOrDefault();
if (string.IsNullOrEmpty(authHeaderValue))
return Task.FromResult(AuthenticateResult.NoResult());
if (!authHeaderValue.Contains("Bearer "))
return Task.FromResult(AuthenticateResult.NoResult());
var providedToken = authHeaderValue
.Replace("Bearer ", "")
.Trim();
if (providedToken != Options.Token)
return Task.FromResult(AuthenticateResult.NoResult());
return Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(
new ClaimsPrincipal(
new ClaimsIdentity("token")
),
"token"
)
));
}
}

View File

@@ -1,46 +0,0 @@
using System.Net.Sockets;
using System.Text.Json;
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models.UnsafeDocker;
namespace MoonlightServers.Daemon.Helpers;
[Singleton]
public class UnsafeDockerClient
{
private readonly AppConfiguration Configuration;
public UnsafeDockerClient(AppConfiguration configuration)
{
Configuration = configuration;
}
public Task<HttpClient> CreateHttpClientAsync()
{
var client = new HttpClient(new SocketsHttpHandler()
{
ConnectCallback = async (context, token) =>
{
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
var endpoint = new UnixDomainSocketEndPoint(
Formatter.ReplaceStart(Configuration.Docker.Uri, "unix://", "")
);
await socket.ConnectAsync(endpoint, token);
return new NetworkStream(socket, ownsSocket: true);
}
});
return Task.FromResult(client);
}
public async Task<DataUsageResponse> GetDataUsageAsync()
{
using var client = await CreateHttpClientAsync();
var responseJson = await client.GetStringAsync("http://some.random.domain/v1.47/system/df");
var response = JsonSerializer.Deserialize<DataUsageResponse>(responseJson)!;
return response;
}
}

View File

@@ -1,77 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/{id:int}")]
public class PowerController : Controller
{
private readonly ServerService ServerService;
public PowerController(ServerService serverService)
{
ServerService = serverService;
}
[HttpPost("start")]
public async Task<ActionResult> StartAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Start))
return Problem("Cannot fire start trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Start);
return NoContent();
}
[HttpPost("stop")]
public async Task<ActionResult> StopAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Stop))
return Problem("Cannot fire stop trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Stop);
return NoContent();
}
[HttpPost("kill")]
public async Task<ActionResult> KillAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Kill))
return Problem("Cannot fire kill trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Kill);
return NoContent();
}
[HttpPost("install")]
public async Task<ActionResult> InstallAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Install))
return Problem("Cannot fire install trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Install);
return NoContent();
}
}

View File

@@ -1,67 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/{id:int}")]
public class ServersController : Controller
{
private readonly ServerService ServerService;
private readonly ServerConfigurationMapper ConfigurationMapper;
public ServersController(ServerService serverService, ServerConfigurationMapper configurationMapper)
{
ServerService = serverService;
ConfigurationMapper = configurationMapper;
}
[HttpPost("sync")]
public async Task<ActionResult> SyncAsync([FromRoute] int id)
{
await ServerService.InitializeByIdAsync(id);
return NoContent();
}
[HttpGet("status")]
public async Task<ActionResult<ServerStatusResponse>> StatusAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
return new ServerStatusResponse()
{
State = (ServerState)server.StateMachine.State
};
}
[HttpGet("logs")]
public async Task<ActionResult<ServerLogsResponse>> LogsAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var messages = await server.Console.GetCacheAsync();
return new ServerLogsResponse()
{
Messages = messages.ToArray()
};
}
[HttpGet("stats")]
public async Task<ServerStatsResponse> GetStatsAsync([FromRoute] int id)
{
return new ServerStatsResponse()
{
};
}
}

View File

@@ -1,54 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
[Authorize]
[ApiController]
[Route("api/statistics")]
public class StatisticsController : Controller
{
private readonly HostSystemHelper HostSystemHelper;
public StatisticsController(HostSystemHelper hostSystemHelper)
{
HostSystemHelper = hostSystemHelper;
}
[HttpGet]
public async Task<StatisticsResponse> GetAsync()
{
var response = new StatisticsResponse();
var cpuUsage = await HostSystemHelper.GetCpuUsageAsync();
response.Cpu.Model = cpuUsage.Model;
response.Cpu.Usage = cpuUsage.OverallUsage;
response.Cpu.UsagePerCore = cpuUsage.PerCoreUsage;
var memoryUsage = await HostSystemHelper.GetMemoryUsageAsync();
response.Memory.Available = memoryUsage.Available;
response.Memory.Cached = memoryUsage.Cached;
response.Memory.Free = memoryUsage.Free;
response.Memory.Total = memoryUsage.Total;
response.Memory.SwapTotal = memoryUsage.SwapTotal;
response.Memory.SwapFree = memoryUsage.SwapFree;
var diskDetails = await HostSystemHelper.GetDiskUsagesAsync();
response.Disks = diskDetails.Select(x => new StatisticsResponse.DiskData()
{
Device = x.Device,
MountPath = x.MountPath,
DiskFree = x.DiskFree,
DiskTotal = x.DiskTotal,
InodesFree = x.InodesFree,
InodesTotal = x.InodesTotal
}).ToArray();
return response;
}
}

View File

@@ -1,38 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
// This controller hosts endpoints for the statistics for the docker environment
[Authorize]
[ApiController]
[Route("api/statistics/docker")]
public class StatisticsDockerController : Controller
{
private readonly DockerInfoService DockerInfoService;
public StatisticsDockerController(DockerInfoService dockerInfoService)
{
DockerInfoService = dockerInfoService;
}
[HttpGet]
public async Task<StatisticsDockerResponse> GetAsync()
{
var usage = await DockerInfoService.GetDataUsageAsync();
return new StatisticsDockerResponse
{
Version = await DockerInfoService.GetDockerVersionAsync(),
ContainersReclaimable = usage.Containers.Reclaimable,
ContainersUsed = usage.Containers.Used,
BuildCacheReclaimable = usage.BuildCache.Reclaimable,
BuildCacheUsed = usage.BuildCache.Used,
ImagesUsed = usage.Images.Used,
ImagesReclaimable = usage.Images.Reclaimable
};
}
}

View File

@@ -1,56 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Sys;
namespace MoonlightServers.Daemon.Http.Controllers.Sys;
[Authorize]
[ApiController]
[Route("api/system/status")]
public class SystemStatusController : Controller
{
private readonly RemoteService RemoteService;
public SystemStatusController(RemoteService remoteService)
{
RemoteService = remoteService;
}
public async Task<SystemStatusResponse> GetAsync()
{
SystemStatusResponse response;
var sw = new Stopwatch();
sw.Start();
try
{
await RemoteService.GetStatusAsync();
sw.Stop();
response = new()
{
TripSuccess = true,
TripTime = sw.Elapsed,
Version = "2.1.0" // TODO: Set global
};
}
catch (Exception e)
{
sw.Stop();
response = new()
{
TripError = e.Message,
TripTime = sw.Elapsed,
TripSuccess = false,
Version = "2.1.0" // TODO: Set global
};
}
return response;
}
}

View File

@@ -1,28 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace MoonlightServers.Daemon.Http.Hubs;
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverWebsocket")]
public class ServerWebSocketHub : Hub
{
private readonly ILogger<ServerWebSocketHub> Logger;
public ServerWebSocketHub(ILogger<ServerWebSocketHub> logger)
{
Logger = logger;
}
public override async Task OnConnectedAsync()
{
// The policies validated already the type and the token so we can assume we are authenticated
// and just start adding ourselves into the desired group
var serverId = Context.User!.Claims.First(x => x.Type == "serverId").Value;
await Groups.AddToGroupAsync(
Context.ConnectionId,
serverId
);
}
}

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