47 Commits
main ... v2.1

Author SHA1 Message Date
ecf0e01914 Merge pull request 'Implemented node system statistics' (#2) from feat/AddNodeSysStatistics into v2.1
All checks were successful
Dev Publish: Nuget / Publish MoonlightServers.Api (push) Successful in 22s
Dev Publish: Nuget / Publish MoonlightServers.DaemonShared (push) Successful in 12s
Dev Publish: Nuget / Publish MoonlightServers.Frontend (push) Successful in 36s
Dev Publish: Nuget / Publish MoonlightServers.Shared (push) Successful in 12s
Reviewed-on: #2
2026-03-21 18:22:19 +00:00
6d447a0ff9 Implemented node system statistics 2026-03-21 18:21:09 +00:00
ba5e364c05 Merge pull request 'Adjusted project to be compatible with the latest moonlight refactoring' (#1) from feat/AdjustToParentRefactor into v2.1
All checks were successful
Dev Publish: Nuget / Publish MoonlightServers.Api (push) Successful in 22s
Dev Publish: Nuget / Publish MoonlightServers.DaemonShared (push) Successful in 11s
Dev Publish: Nuget / Publish MoonlightServers.Frontend (push) Successful in 37s
Dev Publish: Nuget / Publish MoonlightServers.Shared (push) Successful in 12s
Reviewed-on: #1
2026-03-13 08:23:02 +00:00
2a2ce28b5f Refactored imports to use the latest moonlight core structure 2026-03-13 09:21:35 +01:00
91887ec047 Renamed migration table. Added nullability to template variable migration 2026-03-12 15:45:51 +00:00
2fc371c219 Fixed typo and wrong type reference in DataContext 2026-03-12 15:01:19 +00:00
9470e06c0f Changed migration history table to specific schema. Added log for applied migrations 2026-03-12 14:43:04 +00:00
2f8665f1d4 Added explicit migration assembly marker for DataContext 2026-03-12 14:11:46 +00:00
4d4f35e2be Fixed default destination variable assignment for style build 2026-03-12 13:59:39 +00:00
609ea3a443 Added nuget package settings. Fixed style building
All checks were successful
Dev Publish: Nuget / Publish MoonlightServers.Api (push) Successful in 17s
Dev Publish: Nuget / Publish MoonlightServers.DaemonShared (push) Successful in 11s
Dev Publish: Nuget / Publish MoonlightServers.Frontend (push) Successful in 32s
Dev Publish: Nuget / Publish MoonlightServers.Shared (push) Successful in 11s
2026-03-12 13:53:30 +00:00
3e19b29cde Removed no restore for frontend host project as it would not be restored by default 2026-03-12 13:35:52 +00:00
1475b89660 Updated workflow to handle sources aithout persisting them 2026-03-12 13:31:49 +00:00
3bb9a08630 Added missing moonlight nuget source 2026-03-12 13:20:49 +00:00
252c4103f3 Added workflow for building nuget packages 2026-03-12 13:16:50 +00:00
e7b1e77d0a Implemented template crud, db entities, import/export, ptero and pelican import 2026-03-12 13:00:32 +00:00
7c5dc657dc Implemented node crud and status health check. Added daemon status health endpoint. Refactored project structure. Added sidebar items and ui views 2026-03-05 10:56:52 +00:00
2d1b48b0d4 Implemented statistics. Refactored storage abstractions. Added config options for docker and local storage. Added server service and server updating. 2026-03-02 15:51:05 +00:00
52dbd13fb5 Recreated plugin with new project template. Started implementing server system daemon 2026-03-01 21:09:29 +01:00
f6b71f4de6 Upgraded mooncore. Done required refactoring to function with new version 2025-10-20 19:27:31 +00:00
85392208c4 Updated to latest moonlight and mooncore version. Done refactoring to async scheme and other changes. Recreated database migrations and cleaned models 2025-09-22 12:13:57 +02:00
91fb15a03e Started implementing server service and daemon controllers 2025-09-15 21:47:07 +02:00
32f447d268 Deed debug handler. Added installation handler. Improved docker console streaming 2025-09-13 20:53:03 +02:00
160446eed0 Added online detection handler 2025-09-09 23:08:01 +02:00
b90100d250 Implemented restorer, runtime and dummy statistics. Added service registering and fixed server factory. Moved logger to server context 2025-09-07 23:15:48 +02:00
282096595d Improved comments. Started implementing docker components and other base components. Updated dependencies 2025-09-06 21:44:22 +02:00
348e9560ab Cleaned up interfaces. Extracted server state machine trigger handler to seperated classes. Removed legacy code 2025-09-06 15:34:35 +02:00
7587a7e8e3 Cleaned up project files from legacy plugin settings 2025-08-24 11:51:09 +02:00
7c2bc9d19b Improved logging in server components 2025-08-02 21:54:58 +02:00
2e4c933fbe Started implementing server deletion 2025-08-02 21:12:38 +02:00
5c170935b4 Implemented online detection. Extended ServerContext to include self reference so sub components can subscribe to the state. Improved console module detach handling. Implemented new server service to replace the old one. Added log restore when restoring 2025-07-30 20:52:24 +02:00
eaf8c36f7f Fixed event/observer issues 2025-07-30 17:12:21 +02:00
bb81ca9674 Implemented first iteration of the docker-based server installer. Added restore functionality for the installer. Wired up for basic installer testing 2025-07-29 22:24:46 +02:00
f57d33cb1e Fixed usage of IAsyncObservable. Added runtime exit handler 2025-07-29 21:14:41 +02:00
b546a168d2 Implemented restorer, wired up for basic testing. Improved abstractions and fixed observer pattern issues 2025-07-26 23:19:57 +02:00
84b3d1caf6 Implemented factory pattern for server abstraction creation. Implemented raw fs and docker provisioner. Implemented docker event service with observer pattern 2025-07-26 19:14:02 +02:00
0bef60dbc8 For extensions of base system like podman and btrfs: Started improving server abstractions to make it more extendable in order to support multiple implementations 2025-07-25 13:45:47 +02:00
bdc4ad8265 Added id ordering as postgres would return the last changed elements first which breaks pagination 2025-07-24 20:24:00 +02:00
431cdcb260 Improved server share permission handling and share ui 2025-07-24 20:19:49 +02:00
1f94752c54 Started improving server shares and general api controller structure 2025-07-24 18:28:10 +02:00
a2db7be26f Improved server header and variables page 2025-07-18 23:17:56 +02:00
265a4b280b Refactored ui. Improved console experience. Added command endpoint 2025-07-18 21:16:52 +02:00
f8c11b2dd8 Updated mooncore version 2025-07-17 22:56:52 +02:00
e83d1351cb Fixed server create ui 2025-07-16 21:29:43 +02:00
61253919cf Refactored frontend to work with the latest mooncore changes 2025-07-16 20:46:45 +02:00
383d4bb24b Improved some route templates 2025-07-15 21:06:40 +02:00
f22f0c0e51 Refactored api server project for latest mooncore changes 2025-07-15 21:04:46 +02:00
514f862a9d Started refactoring to be compatible with the changed nuget packages and the mooncore changes 2025-07-15 19:21:44 +02:00
417 changed files with 10630 additions and 18289 deletions

View File

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

49
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,111 @@
<!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="/_content/Moonlight.Frontend/chart.umd.js" defer></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Shared;
using MoonlightServers.Shared.Admin.Nodes;
namespace MoonlightServers.Api.Admin.Nodes;
[ApiController]
[Authorize(Policy = Permissions.Nodes.View)]
[Route("api/admin/servers/nodes/{id:int}/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]
public async Task<ActionResult<NodeStatisticsDto>> GetAsync([FromRoute] int id)
{
var node = await NodeRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (node == null)
return Problem("No node with this id found", statusCode: 404);
var statistics = await NodeService.GetStatisticsAsync(node);
var dto = NodeMapper.ToDto(statistics);
return dto;
}
}

View File

@@ -0,0 +1,172 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Shared.Shared;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Api.Infrastructure.Database.Json;
using MoonlightServers.Shared;
using MoonlightServers.Shared.Admin.Templates;
namespace MoonlightServers.Api.Admin.Templates;
[ApiController]
[Route("api/admin/servers/templates")]
public class CrudController : Controller
{
private readonly DatabaseRepository<Template> DatabaseRepository;
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
public CrudController(
DatabaseRepository<Template> databaseRepository,
DatabaseRepository<TemplateDockerImage> dockerImageRepository
)
{
DatabaseRepository = databaseRepository;
DockerImageRepository = dockerImageRepository;
}
[HttpGet]
[Authorize(Policy = Permissions.Templates.View)]
public async Task<ActionResult<PagedData<TemplateDto>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int length,
[FromQuery] FilterOptions? filterOptions
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
// Query building
var query = DatabaseRepository
.Query();
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Template.Name) =>
query.Where(role => EF.Functions.ILike(role.Name, $"%{filterOption.Value}%")),
_ => query
};
}
}
// Pagination
var data = await query
.OrderBy(x => x.Id)
.ProjectToDto()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<TemplateDto>(data, total);
}
[HttpGet("{id:int}")]
[Authorize(Policy = Permissions.Templates.View)]
public async Task<ActionResult<DetailedTemplateDto>> GetAsync([FromRoute] int id)
{
var template = await DatabaseRepository
.Query()
.Include(x => x.DefaultDockerImage)
.FirstOrDefaultAsync(x => x.Id == id);
if (template == null)
return Problem("No template with this id found", statusCode: 404);
return TemplateMapper.ToDetailedDto(template);
}
[HttpPost]
[Authorize(Policy = Permissions.Templates.Create)]
public async Task<ActionResult<TemplateDto>> CreateAsync([FromBody] CreateTemplateDto request)
{
var template = TemplateMapper.ToEntity(request);
// Fill in default values
template.LifecycleConfig = new()
{
StartupCommands = [
new StartupCommand
{
DisplayName = "Default Startup",
Command = "bash startup.sh"
}
],
StopCommand = "^C",
OnlineLogPatterns = ["I am online"]
};
template.InstallationConfig = new()
{
DockerImage = "debian",
Script = "#!/bin/bash\necho Installing",
Shell = "/bin/bash"
};
template.FilesConfig = new()
{
ConfigurationFiles = []
};
template.MiscellaneousConfig = new()
{
UseLegacyStartup = true
};
var finalRole = await DatabaseRepository.AddAsync(template);
return TemplateMapper.ToDto(finalRole);
}
[HttpPut("{id:int}")]
[Authorize(Policy = Permissions.Templates.Edit)]
public async Task<ActionResult<DetailedTemplateDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateTemplateDto request)
{
var template = await DatabaseRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (template == null)
return Problem("No template with this id found", statusCode: 404);
TemplateMapper.Merge(template, request);
template.DefaultDockerImage = await DockerImageRepository
.Query()
.Where(x => x.Template.Id == id)
.FirstOrDefaultAsync(x => x.Id == request.DefaultDockerImageId);
await DatabaseRepository.UpdateAsync(template);
return TemplateMapper.ToDetailedDto(template);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = Permissions.Templates.Delete)]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var template = await DatabaseRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (template == null)
return Problem("No template with this id found", statusCode: 404);
await DatabaseRepository.RemoveAsync(template);
return NoContent();
}
}

View File

@@ -0,0 +1,144 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Shared.Shared;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Shared;
using MoonlightServers.Shared.Admin.Templates;
namespace MoonlightServers.Api.Admin.Templates;
[ApiController]
[Route("api/admin/servers/templates/{templateId:int}/dockerImages")]
public class DockerImagesController : Controller
{
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
private readonly DatabaseRepository<Template> TemplateRepository;
public DockerImagesController(
DatabaseRepository<TemplateDockerImage> dockerImageRepository,
DatabaseRepository<Template> templateRepository
)
{
DockerImageRepository = dockerImageRepository;
TemplateRepository = templateRepository;
}
[HttpGet]
[Authorize(Policy = Permissions.Templates.View)]
public async Task<ActionResult<PagedData<DockerImageDto>>> GetAsync(
[FromRoute] int templateId,
[FromQuery] int startIndex,
[FromQuery] int length
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
if (!await TemplateRepository.Query().AnyAsync(x => x.Id == templateId))
return Problem("No template with that id found", statusCode: 404);
// Query building
var query = DockerImageRepository
.Query()
.Where(x => x.Template.Id == templateId);
// Pagination
var data = await query
.OrderBy(x => x.Id)
.ProjectToDto()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<DockerImageDto>(data, total);
}
[HttpGet("{id:int}")]
[Authorize(Policy = Permissions.Templates.View)]
public async Task<ActionResult<DockerImageDto>> GetAsync(
[FromRoute] int templateId,
[FromRoute] int id
)
{
var templateDockerImage = await DockerImageRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
if (templateDockerImage == null)
return Problem("No template or template dockerImage found with that id");
return TemplateMapper.ToDto(templateDockerImage);
}
[HttpPost]
[Authorize(Policy = Permissions.Templates.Create)]
public async Task<ActionResult<DockerImageDto>> CreateAsync(
[FromRoute] int templateId,
[FromBody] CreateDockerImageDto dto
)
{
var template = await TemplateRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == templateId);
if (template == null)
return Problem("No template with that id found", statusCode: 404);
var dockerImage = TemplateMapper.ToEntity(dto);
dockerImage.Template = template;
var finalDockerImage = await DockerImageRepository.AddAsync(dockerImage);
return TemplateMapper.ToDto(finalDockerImage);
}
[HttpPut("{id:int}")]
[Authorize(Policy = Permissions.Templates.Edit)]
public async Task<ActionResult<DockerImageDto>> UpdateAsync(
[FromRoute] int templateId,
[FromRoute] int id,
[FromBody] UpdateDockerImageDto dto
)
{
var templateDockerImage = await DockerImageRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
if (templateDockerImage == null)
return Problem("No template or template dockerImage found with that id");
TemplateMapper.Merge(templateDockerImage, dto);
await DockerImageRepository.UpdateAsync(templateDockerImage);
return TemplateMapper.ToDto(templateDockerImage);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = Permissions.Templates.Delete)]
public async Task<ActionResult> DeleteAsync(
[FromRoute] int templateId,
[FromRoute] int id
)
{
var templateDockerImage = await DockerImageRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
if (templateDockerImage == null)
return Problem("No template or template dockerImage found with that id");
await DockerImageRepository.RemoveAsync(templateDockerImage);
return NoContent();
}
}

View File

@@ -0,0 +1,207 @@
using System.Text;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Api.Infrastructure.Database.Json;
using VYaml.Annotations;
using VYaml.Serialization;
namespace MoonlightServers.Api.Admin.Templates;
public class PelicanEggImportService
{
private readonly DatabaseRepository<Template> TemplateRepository;
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
public PelicanEggImportService(
DatabaseRepository<Template> templateRepository,
DatabaseRepository<TemplateDockerImage> dockerImageRepository
)
{
TemplateRepository = templateRepository;
DockerImageRepository = dockerImageRepository;
}
public async Task<Template> ImportAsync(string content)
{
var egg = YamlSerializer.Deserialize<Egg>(Encoding.UTF8.GetBytes(content));
var template = new Template()
{
AllowUserDockerImageChange = true,
Author = egg.Author,
Description = egg.Description,
DonateUrl = null,
Name = egg.Name,
UpdateUrl = egg.Meta.UpdateUrl,
Version = "1.0.0",
FilesConfig = new FilesConfig()
{
ConfigurationFiles = egg.Config.Files.Select(file => new ConfigurationFile()
{
Path = file.Key,
Parser = file.Value.Parser,
Mappings = file.Value.Find.Select(pair => new ConfigurationFileMapping()
{
Key = pair.Key,
Value = pair.Value
}).ToList()
}).ToList()
},
InstallationConfig = new InstallationConfig()
{
DockerImage = egg.Scripts.Installation.Container,
Script = egg.Scripts.Installation.Script,
Shell = egg.Scripts.Installation.Entrypoint
},
LifecycleConfig = new LifecycleConfig()
{
OnlineLogPatterns = egg.Config.Startup.Values.ToList(),
StopCommand = egg.Config.Stop,
StartupCommands = egg.StartupCommands.Select(x => new StartupCommand()
{
DisplayName = x.Key,
Command = x.Value
}).ToList()
},
MiscellaneousConfig = new MiscellaneousConfig()
{
UseLegacyStartup = true
},
Variables = egg.Variables.Select(variable => new TemplateVariable()
{
Description = variable.Description,
DisplayName = variable.Name,
DefaultValue = variable.DefaultValue,
EnvName = variable.EnvVariable
}).ToList()
};
var finalTemplate = await TemplateRepository.AddAsync(template);
var isFirst = true;
TemplateDockerImage? defaultDockerImage = null;
foreach (var dockerImage in egg.DockerImages)
{
var finalDockerImage = await DockerImageRepository.AddAsync(new TemplateDockerImage()
{
DisplayName = dockerImage.Key,
ImageName = dockerImage.Value,
SkipPulling = false,
Template = finalTemplate
});
if (isFirst)
{
isFirst = false;
defaultDockerImage = finalDockerImage;
}
}
finalTemplate.DefaultDockerImage = defaultDockerImage;
await TemplateRepository.UpdateAsync(finalTemplate);
return finalTemplate;
}
}
[YamlObject]
public partial class Egg
{
[YamlMember("_comment")] public string? Comment { get; set; }
[YamlMember("meta")] public EggMeta Meta { get; set; } = new();
[YamlMember("exported_at")] public string? ExportedAt { get; set; }
[YamlMember("name")] public string Name { get; set; } = string.Empty;
[YamlMember("author")] public string Author { get; set; } = string.Empty;
[YamlMember("uuid")] public string Uuid { get; set; } = string.Empty;
[YamlMember("description")] public string Description { get; set; } = string.Empty;
[YamlMember("image")] public string? Image { get; set; }
[YamlMember("tags")] public List<string> Tags { get; set; } = new();
[YamlMember("features")] public List<string> Features { get; set; } = new();
[YamlMember("docker_images")] public Dictionary<string, string> DockerImages { get; set; } = new();
[YamlMember("file_denylist")] public Dictionary<string, string> FileDenylist { get; set; } = new();
[YamlMember("startup_commands")] public Dictionary<string, string> StartupCommands { get; set; } = new();
[YamlMember("config")] public EggConfig Config { get; set; } = new();
[YamlMember("scripts")] public EggScripts Scripts { get; set; } = new();
[YamlMember("variables")] public List<EggVariable> Variables { get; set; } = new();
}
[YamlObject]
public partial class EggMeta
{
[YamlMember("version")] public string Version { get; set; } = string.Empty;
[YamlMember("update_url")] public string? UpdateUrl { get; set; }
}
[YamlObject]
public partial class EggConfig
{
[YamlMember("files")] public Dictionary<string, EggConfigFile> Files { get; set; } = new();
[YamlMember("startup")] public Dictionary<string, string> Startup { get; set; } = new();
[YamlMember("logs")] public Dictionary<string, string> Logs { get; set; } = new();
[YamlMember("stop")] public string Stop { get; set; } = string.Empty;
}
[YamlObject]
public partial class EggConfigFile
{
[YamlMember("parser")] public string Parser { get; set; } = string.Empty;
[YamlMember("find")] public Dictionary<string, string> Find { get; set; } = new();
}
[YamlObject]
public partial class EggScripts
{
[YamlMember("installation")] public EggInstallationScript Installation { get; set; } = new();
}
[YamlObject]
public partial class EggInstallationScript
{
[YamlMember("script")] public string Script { get; set; } = string.Empty;
[YamlMember("container")] public string Container { get; set; } = string.Empty;
[YamlMember("entrypoint")] public string Entrypoint { get; set; } = string.Empty;
}
[YamlObject]
public partial class EggVariable
{
[YamlMember("name")] public string Name { get; set; } = string.Empty;
[YamlMember("description")] public string Description { get; set; } = string.Empty;
[YamlMember("env_variable")] public string EnvVariable { get; set; } = string.Empty;
[YamlMember("default_value")] public string DefaultValue { get; set; } = string.Empty;
[YamlMember("user_viewable")] public bool UserViewable { get; set; }
[YamlMember("user_editable")] public bool UserEditable { get; set; }
[YamlMember("rules")] public List<string> Rules { get; set; } = new();
[YamlMember("sort")] public int Sort { get; set; }
}

View File

@@ -0,0 +1,255 @@
using System.Text.Json;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Api.Infrastructure.Database.Json;
namespace MoonlightServers.Api.Admin.Templates;
public class PterodactylEggImportService
{
private readonly DatabaseRepository<Template> TemplateRepository;
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
public PterodactylEggImportService(
DatabaseRepository<Template> templateRepository,
DatabaseRepository<TemplateDockerImage> dockerImageRepository
)
{
TemplateRepository = templateRepository;
DockerImageRepository = dockerImageRepository;
}
public async Task<Template> ImportAsync(string json)
{
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var template = new Template
{
Name = Truncate(root.GetStringOrDefault("name") ?? "Unknown", 30),
Description = Truncate(root.GetStringOrDefault("description") ?? "", 255),
Author = Truncate(root.GetStringOrDefault("author") ?? "", 30),
Version = "1.0.0",
UpdateUrl = root.TryGetProperty("meta", out var meta)
? meta.GetStringOrDefault("update_url")
: null,
DonateUrl = null,
FilesConfig = ParseFilesConfig(root),
LifecycleConfig = ParseLifecycleConfig(root),
InstallationConfig = ParseInstallationConfig(root),
MiscellaneousConfig = new MiscellaneousConfig { UseLegacyStartup = true },
AllowUserDockerImageChange = true,
Variables = ParseVariables(root)
};
var finalTemplate = await TemplateRepository.AddAsync(template);
var dockerImages = ParseDockerImageModels(root);
TemplateDockerImage? defaultDockerImage = null;
var isFirst = true;
foreach (var (displayName, imageName) in dockerImages)
{
var entity = new TemplateDockerImage
{
DisplayName = displayName,
ImageName = imageName,
SkipPulling = false,
Template = finalTemplate
};
var finalEntity = await DockerImageRepository.AddAsync(entity);
if (isFirst)
{
isFirst = false;
defaultDockerImage = finalEntity;
}
}
finalTemplate.DefaultDockerImage = defaultDockerImage;
await TemplateRepository.UpdateAsync(finalTemplate);
return finalTemplate;
}
private static FilesConfig ParseFilesConfig(JsonElement root)
{
var configFiles = new List<ConfigurationFile>();
if (!root.TryGetProperty("config", out var config))
return new FilesConfig { ConfigurationFiles = configFiles };
if (!config.TryGetProperty("files", out var filesElement))
return new FilesConfig { ConfigurationFiles = configFiles };
var filesJson = filesElement.ValueKind == JsonValueKind.String
? filesElement.GetString()
: filesElement.GetRawText();
if (string.IsNullOrWhiteSpace(filesJson) || filesJson == "{}" || filesJson == "[]")
return new FilesConfig { ConfigurationFiles = configFiles };
try
{
using var filesDoc = JsonDocument.Parse(filesJson);
foreach (var fileProperty in filesDoc.RootElement.EnumerateObject())
{
var parser = fileProperty.Value.GetStringOrDefault("parser") ?? "json";
var mappings = new List<ConfigurationFileMapping>();
if (fileProperty.Value.TryGetProperty("find", out var find))
{
foreach (var mapping in find.EnumerateObject())
{
mappings.Add(new ConfigurationFileMapping
{
Key = mapping.Name,
Value = mapping.Value.ValueKind == JsonValueKind.String
? mapping.Value.GetString() ?? ""
: mapping.Value.GetRawText()
});
}
}
configFiles.Add(new ConfigurationFile
{
Path = fileProperty.Name,
Parser = parser,
Mappings = mappings
});
}
}
catch (JsonException)
{
}
return new FilesConfig { ConfigurationFiles = configFiles };
}
private static LifecycleConfig ParseLifecycleConfig(JsonElement root)
{
var stopCommand = "";
var onlinePatterns = new List<string>();
if (root.TryGetProperty("config", out var config))
{
stopCommand = config.GetStringOrDefault("stop") ?? "";
if (config.TryGetProperty("startup", out var startupElement))
{
var startupJson = startupElement.ValueKind == JsonValueKind.String
? startupElement.GetString()
: startupElement.GetRawText();
if (!string.IsNullOrWhiteSpace(startupJson))
{
try
{
using var startupDoc = JsonDocument.Parse(startupJson);
if (startupDoc.RootElement.TryGetProperty("done", out var done))
{
var doneValue = done.ValueKind == JsonValueKind.String
? done.GetString()
: done.GetRawText();
if (!string.IsNullOrWhiteSpace(doneValue))
onlinePatterns.Add(doneValue);
}
}
catch (JsonException)
{
}
}
}
}
return new LifecycleConfig
{
StartupCommands =
[
new StartupCommand
{
DisplayName = "Startup",
Command = root.GetStringOrDefault("startup") ?? ""
}
],
StopCommand = stopCommand,
OnlineLogPatterns = onlinePatterns
};
}
private static InstallationConfig ParseInstallationConfig(JsonElement root)
{
if (!root.TryGetProperty("scripts", out var scripts))
return new InstallationConfig();
if (!scripts.TryGetProperty("installation", out var installation))
return new InstallationConfig();
return new InstallationConfig
{
DockerImage = installation.GetStringOrDefault("container") ?? "",
Shell = installation.GetStringOrDefault("entrypoint") ?? "bash",
Script = installation.GetStringOrDefault("script") ?? ""
};
}
// Returns (DisplayName, ImageName, IsFirst) tuples to avoid a temporary model
private static List<(string DisplayName, string ImageName)> ParseDockerImageModels(JsonElement root)
{
var result = new List<(string, string)>();
if (!root.TryGetProperty("docker_images", out var dockerImages))
return result;
foreach (var img in dockerImages.EnumerateObject())
{
result.Add((
Truncate(img.Name, 30),
Truncate(img.Value.GetString() ?? img.Name, 255)
));
}
return result;
}
private static List<TemplateVariable> ParseVariables(JsonElement root)
{
var variables = new List<TemplateVariable>();
if (!root.TryGetProperty("variables", out var vars))
return variables;
foreach (var v in vars.EnumerateArray())
{
variables.Add(new TemplateVariable
{
DisplayName = Truncate(v.GetStringOrDefault("name") ?? "Variable", 30),
Description = Truncate(v.GetStringOrDefault("description") ?? "", 255),
EnvName = Truncate(v.GetStringOrDefault("env_variable") ?? "", 60),
DefaultValue = Truncate(v.GetStringOrDefault("default_value") ?? "", 1024)
});
}
return variables;
}
private static string Truncate(string value, int maxLength) =>
value.Length <= maxLength ? value : value[..maxLength];
}
internal static class JsonElementExtensions
{
public static string? GetStringOrDefault(this JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String
? prop.GetString()
: null;
}
}

View File

@@ -0,0 +1,28 @@
using System.Diagnostics.CodeAnalysis;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Shared.Admin.Templates;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.Api.Admin.Templates;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class TemplateMapper
{
public static partial TemplateDto ToDto(Template template);
public static partial DetailedTemplateDto ToDetailedDto(Template template);
public static partial IQueryable<TemplateDto> ProjectToDto(this IQueryable<Template> templates);
public static partial Template ToEntity(CreateTemplateDto dto);
public static partial void Merge([MappingTarget] Template template, UpdateTemplateDto dto);
public static partial IQueryable<VariableDto> ProjectToDto(this IQueryable<TemplateVariable> variables);
public static partial VariableDto ToDto(TemplateVariable variable);
public static partial TemplateVariable ToEntity(CreateVariableDto dto);
public static partial void Merge([MappingTarget] TemplateVariable variable, UpdateVariableDto dto);
public static partial IQueryable<DockerImageDto> ProjectToDto(this IQueryable<TemplateDockerImage> dockerImages);
public static partial DockerImageDto ToDto(TemplateDockerImage dockerImage);
public static partial TemplateDockerImage ToEntity(CreateDockerImageDto dto);
public static partial void Merge([MappingTarget] TemplateDockerImage dockerImage, UpdateDockerImageDto dto);
}

View File

@@ -0,0 +1,92 @@
using VYaml.Annotations;
namespace MoonlightServers.Api.Admin.Templates;
[YamlObject]
public partial class TemplateTransferModel
{
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public string Author { get; set; } = "";
public string Version { get; set; } = "";
public string? UpdateUrl { get; set; }
public string? DonateUrl { get; set; }
public FilesConfigTransferModel Files { get; set; } = new();
public LifecycleConfigTransferModel Lifecycle { get; set; } = new();
public InstallationConfigTransferModel Installation { get; set; } = new();
public MiscellaneousConfigTransferModel Miscellaneous { get; set; } = new();
public bool AllowUserDockerImageChange { get; set; }
public List<TemplateDockerImageTransferModel> DockerImages { get; set; } = new();
public List<TemplateVariableTransferModel> Variables { get; set; } = new();
}
[YamlObject]
public partial class TemplateDockerImageTransferModel
{
public string DisplayName { get; set; } = "";
public string ImageName { get; set; } = "";
public bool SkipPulling { get; set; }
public bool IsDefault { get; set; }
}
[YamlObject]
public partial class TemplateVariableTransferModel
{
public string DisplayName { get; set; } = "";
public string Description { get; set; } = "";
public string EnvName { get; set; } = "";
public string? DefaultValue { get; set; }
}
[YamlObject]
public partial class FilesConfigTransferModel
{
public List<ConfigurationFileTransferModel> ConfigurationFiles { get; set; } = new();
}
[YamlObject]
public partial class ConfigurationFileTransferModel
{
public string Path { get; set; } = "";
public string Parser { get; set; } = "";
public List<ConfigurationFileMappingTransferModel> Mappings { get; set; } = new();
}
[YamlObject]
public partial class ConfigurationFileMappingTransferModel
{
public string Key { get; set; } = "";
public string? Value { get; set; }
}
[YamlObject]
public partial class LifecycleConfigTransferModel
{
public List<StartupCommandTransferModel> StartupCommands { get; set; } = new();
public string StopCommand { get; set; } = "";
public List<string> OnlineLogPatterns { get; set; } = new();
}
[YamlObject]
public partial class StartupCommandTransferModel
{
public string DisplayName { get; set; } = "";
public string Command { get; set; } = "";
}
[YamlObject]
public partial class InstallationConfigTransferModel
{
public string DockerImage { get; set; } = "";
public string Shell { get; set; } = "";
public string Script { get; set; } = "";
}
[YamlObject]
public partial class MiscellaneousConfigTransferModel
{
public bool UseLegacyStartup { get; set; }
}

View File

@@ -0,0 +1,182 @@
using Microsoft.EntityFrameworkCore;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Api.Infrastructure.Database.Json;
namespace MoonlightServers.Api.Admin.Templates;
public class TemplateTransferService
{
private readonly DatabaseRepository<Template> TemplateRepository;
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
public TemplateTransferService(DatabaseRepository<Template> templateRepository,
DatabaseRepository<TemplateDockerImage> dockerImageRepository)
{
TemplateRepository = templateRepository;
DockerImageRepository = dockerImageRepository;
}
public async Task<TemplateTransferModel?> ExportAsync(int id)
{
var template = await TemplateRepository
.Query()
.Include(x => x.Variables)
.Include(x => x.DockerImages)
.Include(x => x.DefaultDockerImage)
.FirstOrDefaultAsync(x => x.Id == id);
if (template == null)
return null;
return new()
{
Name = template.Name,
Description = template.Description,
Author = template.Author,
Version = template.Version,
UpdateUrl = template.UpdateUrl,
DonateUrl = template.DonateUrl,
Files = new FilesConfigTransferModel
{
ConfigurationFiles = template.FilesConfig.ConfigurationFiles
.Select(cf => new ConfigurationFileTransferModel
{
Path = cf.Path,
Parser = cf.Parser,
Mappings = cf.Mappings
.Select(m => new ConfigurationFileMappingTransferModel { Key = m.Key, Value = m.Value })
.ToList()
})
.ToList()
},
Lifecycle = new LifecycleConfigTransferModel
{
StartupCommands = template.LifecycleConfig.StartupCommands
.Select(sc => new StartupCommandTransferModel
{ DisplayName = sc.DisplayName, Command = sc.Command })
.ToList(),
StopCommand = template.LifecycleConfig.StopCommand,
OnlineLogPatterns = template.LifecycleConfig.OnlineLogPatterns.ToList()
},
Installation = new InstallationConfigTransferModel
{
DockerImage = template.InstallationConfig.DockerImage,
Shell = template.InstallationConfig.Shell,
Script = template.InstallationConfig.Script
},
Miscellaneous = new MiscellaneousConfigTransferModel
{
UseLegacyStartup = template.MiscellaneousConfig.UseLegacyStartup
},
AllowUserDockerImageChange = template.AllowUserDockerImageChange,
DockerImages = template.DockerImages
.Select(img => new TemplateDockerImageTransferModel
{
DisplayName = img.DisplayName,
ImageName = img.ImageName,
SkipPulling = img.SkipPulling,
IsDefault = template.DefaultDockerImage != null && img.Id == template.DefaultDockerImage.Id
})
.ToList(),
Variables = template.Variables
.Select(v => new TemplateVariableTransferModel
{
DisplayName = v.DisplayName,
Description = v.Description,
EnvName = v.EnvName,
DefaultValue = v.DefaultValue
})
.ToList()
};
}
public async Task<Template> ImportAsync(TemplateTransferModel m)
{
var template = new Template
{
Name = m.Name,
Description = m.Description,
Author = m.Author,
Version = m.Version,
UpdateUrl = m.UpdateUrl,
DonateUrl = m.DonateUrl,
FilesConfig = new FilesConfig
{
ConfigurationFiles = m.Files.ConfigurationFiles
.Select(cf => new ConfigurationFile
{
Path = cf.Path,
Parser = cf.Parser,
Mappings = cf.Mappings
.Select(mp => new ConfigurationFileMapping { Key = mp.Key, Value = mp.Value })
.ToList()
})
.ToList()
},
LifecycleConfig = new LifecycleConfig
{
StartupCommands = m.Lifecycle.StartupCommands
.Select(sc => new StartupCommand { DisplayName = sc.DisplayName, Command = sc.Command })
.ToList(),
StopCommand = m.Lifecycle.StopCommand,
OnlineLogPatterns = m.Lifecycle.OnlineLogPatterns.ToList()
},
InstallationConfig = new InstallationConfig
{
DockerImage = m.Installation.DockerImage,
Shell = m.Installation.Shell,
Script = m.Installation.Script
},
MiscellaneousConfig = new MiscellaneousConfig { UseLegacyStartup = m.Miscellaneous.UseLegacyStartup },
AllowUserDockerImageChange = m.AllowUserDockerImageChange,
Variables = m.Variables
.Select(v => new TemplateVariable
{
DisplayName = v.DisplayName,
Description = v.Description,
EnvName = v.EnvName,
DefaultValue = v.DefaultValue
})
.ToList()
};
var finalTemplate = await TemplateRepository.AddAsync(template);
TemplateDockerImage? defaultDockerImage = null;
foreach (var img in m.DockerImages)
{
var entity = new TemplateDockerImage
{
DisplayName = img.DisplayName,
ImageName = img.ImageName,
SkipPulling = img.SkipPulling,
Template = template
};
var finalEntity = await DockerImageRepository.AddAsync(entity);
if (img.IsDefault)
defaultDockerImage = finalEntity;
}
finalTemplate.DefaultDockerImage = defaultDockerImage;
await TemplateRepository.UpdateAsync(finalTemplate);
return finalTemplate;
}
}

View File

@@ -0,0 +1,88 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using MoonlightServers.Shared.Admin.Templates;
using VYaml.Serialization;
namespace MoonlightServers.Api.Admin.Templates;
[ApiController]
[Route("api/admin/servers/templates")]
public class TransferController : Controller
{
private readonly TemplateTransferService TransferService;
public TransferController(TemplateTransferService transferService)
{
TransferService = transferService;
}
[HttpGet("{id:int}/export")]
public async Task<ActionResult> ExportAsync([FromRoute] int id)
{
var transferModel = await TransferService.ExportAsync(id);
if (transferModel == null)
return Problem("No template with that id found", statusCode: 404);
var yml = YamlSerializer.Serialize(transferModel, new YamlSerializerOptions
{
Resolver = CompositeResolver.Create([
GeneratedResolver.Instance,
StandardResolver.Instance
])
});
return File(yml.ToArray(), "text/yaml", $"{transferModel.Name}.yml");
}
[HttpPost("import")]
public async Task<ActionResult<TemplateDto>> ImportAsync()
{
string content;
await using (Stream receiveStream = Request.Body)
using (StreamReader readStream = new StreamReader(receiveStream))
content = await readStream.ReadToEndAsync();
if(content.Contains("version: PLCN_v3"))
{
var importService = HttpContext.RequestServices.GetRequiredService<PelicanEggImportService>();
var template = await importService.ImportAsync(content);
return TemplateMapper.ToDto(template);
}
if (
content.Contains("PTDL_v2", StringComparison.OrdinalIgnoreCase) ||
content.Contains("PLCN_v1", StringComparison.OrdinalIgnoreCase) ||
content.Contains("PLCN_v2", StringComparison.OrdinalIgnoreCase) ||
content.Contains("PLCN_v3", StringComparison.OrdinalIgnoreCase)
)
{
var importService = HttpContext.RequestServices.GetRequiredService<PterodactylEggImportService>();
var template = await importService.ImportAsync(content);
return TemplateMapper.ToDto(template);
}
else
{
var transferModel = YamlSerializer.Deserialize<TemplateTransferModel>(
Encoding.UTF8.GetBytes(content),
new YamlSerializerOptions
{
Resolver = CompositeResolver.Create([
GeneratedResolver.Instance,
StandardResolver.Instance
])
}
);
var template = await TransferService.ImportAsync(transferModel);
return TemplateMapper.ToDto(template);
}
}
}

View File

@@ -0,0 +1,144 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Shared.Shared;
using MoonlightServers.Api.Infrastructure.Database;
using MoonlightServers.Api.Infrastructure.Database.Entities;
using MoonlightServers.Shared;
using MoonlightServers.Shared.Admin.Templates;
namespace MoonlightServers.Api.Admin.Templates;
[ApiController]
[Route("api/admin/servers/templates/{templateId:int}/variables")]
public class VariablesController : Controller
{
private readonly DatabaseRepository<TemplateVariable> VariableRepository;
private readonly DatabaseRepository<Template> TemplateRepository;
public VariablesController(
DatabaseRepository<TemplateVariable> variableRepository,
DatabaseRepository<Template> templateRepository
)
{
VariableRepository = variableRepository;
TemplateRepository = templateRepository;
}
[HttpGet]
[Authorize(Policy = Permissions.Templates.View)]
public async Task<ActionResult<PagedData<VariableDto>>> GetAsync(
[FromRoute] int templateId,
[FromQuery] int startIndex,
[FromQuery] int length
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
if (!await TemplateRepository.Query().AnyAsync(x => x.Id == templateId))
return Problem("No template with that id found", statusCode: 404);
// Query building
var query = VariableRepository
.Query()
.Where(x => x.Template.Id == templateId);
// Pagination
var data = await query
.OrderBy(x => x.Id)
.ProjectToDto()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<VariableDto>(data, total);
}
[HttpGet("{id:int}")]
[Authorize(Policy = Permissions.Templates.View)]
public async Task<ActionResult<VariableDto>> GetAsync(
[FromRoute] int templateId,
[FromRoute] int id
)
{
var templateVariable = await VariableRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
if (templateVariable == null)
return Problem("No template or template variable found with that id");
return TemplateMapper.ToDto(templateVariable);
}
[HttpPost]
[Authorize(Policy = Permissions.Templates.Create)]
public async Task<ActionResult<VariableDto>> CreateAsync(
[FromRoute] int templateId,
[FromBody] CreateVariableDto dto
)
{
var template = await TemplateRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == templateId);
if (template == null)
return Problem("No template with that id found", statusCode: 404);
var variable = TemplateMapper.ToEntity(dto);
variable.Template = template;
var finalVariable = await VariableRepository.AddAsync(variable);
return TemplateMapper.ToDto(finalVariable);
}
[HttpPut("{id:int}")]
[Authorize(Policy = Permissions.Templates.Edit)]
public async Task<ActionResult<VariableDto>> UpdateAsync(
[FromRoute] int templateId,
[FromRoute] int id,
[FromBody] UpdateVariableDto dto
)
{
var templateVariable = await VariableRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
if (templateVariable == null)
return Problem("No template or template variable found with that id");
TemplateMapper.Merge(templateVariable, dto);
await VariableRepository.UpdateAsync(templateVariable);
return TemplateMapper.ToDto(templateVariable);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = Permissions.Templates.Delete)]
public async Task<ActionResult> DeleteAsync(
[FromRoute] int templateId,
[FromRoute] int id
)
{
var templateVariable = await VariableRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
if (templateVariable == null)
return Problem("No template or template variable found with that id");
await VariableRepository.RemoveAsync(templateVariable);
return NoContent();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
using MoonlightServers.Api.Infrastructure.Database.Json;
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
public class Template
{
public int Id { get; set; }
// Meta
[MaxLength(30)]
public string Name { get; set; }
[MaxLength(255)]
public string Description { get; set; }
[MaxLength(30)]
public string Author { get; set; }
[MaxLength(30)]
public string Version { get; set; }
[MaxLength(2048)]
public string? UpdateUrl { get; set; }
[MaxLength(2048)]
public string? DonateUrl { get; set; }
// JSON Options
public FilesConfig FilesConfig { get; set; }
public LifecycleConfig LifecycleConfig { get; set; }
public InstallationConfig InstallationConfig { get; set; }
public MiscellaneousConfig MiscellaneousConfig { get; set; }
// Docker Images
public bool AllowUserDockerImageChange { get; set; }
public TemplateDockerImage? DefaultDockerImage { get; set; }
public List<TemplateDockerImage> DockerImages { get; set; } = new();
// Variables
public List<TemplateVariable> Variables { get; set; } = new();
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
public class TemplateDockerImage
{
public int Id { get; set; }
[MaxLength(30)]
public string DisplayName { get; set; }
[MaxLength(255)]
public string ImageName { get; set; }
public bool SkipPulling { get; set; }
// Relations
public Template Template { get; set; }
}

View File

@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
public class TemplateVariable
{
public int Id { get; set; }
[MaxLength(30)]
public string DisplayName { get; set; }
[MaxLength(255)]
public string Description { get; set; }
[MaxLength(60)]
public string EnvName { get; set; }
[MaxLength(1024)]
public string? DefaultValue { get; set; }
// Relations
public Template Template { get; set; }
}

View File

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

View File

@@ -0,0 +1,19 @@
namespace MoonlightServers.Api.Infrastructure.Database.Json;
public class FilesConfig
{
public List<ConfigurationFile> ConfigurationFiles { get; set; } = [];
}
public class ConfigurationFile
{
public string Path { get; set; } = string.Empty;
public string Parser { get; set; } = string.Empty;
public List<ConfigurationFileMapping> Mappings { get; set; } = [];
}
public class ConfigurationFileMapping
{
public string Key { get; set; } = string.Empty;
public string? Value { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,8 @@
namespace MoonlightServers.Api.Infrastructure.Database.Json;
public class InstallationConfig
{
public string DockerImage { get; set; } = string.Empty;
public string Shell { get; set; } = string.Empty;
public string Script { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,14 @@
namespace MoonlightServers.Api.Infrastructure.Database.Json;
public class LifecycleConfig
{
public List<StartupCommand> StartupCommands { get; set; } = [];
public string StopCommand { get; set; } = string.Empty;
public List<string> OnlineLogPatterns { get; set; } = [];
}
public class StartupCommand
{
public string DisplayName { get; set; } = string.Empty;
public string Command { get; set; } = string.Empty;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,315 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20260312075719_AddedTemplateEntities")]
partial class AddedTemplateEntities
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("HttpEndpointUrl")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Nodes", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowUserDockerImageChange")
.HasColumnType("boolean");
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int?>("DefaultDockerImageId")
.HasColumnType("integer");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("DonateUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("UpdateUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
{
b1.IsRequired();
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
{
b2.IsRequired();
b2.Property<string>("Parser")
.IsRequired();
b2.Property<string>("Path")
.IsRequired();
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
{
b3.IsRequired();
b3.Property<string>("Key")
.IsRequired();
b3.Property<string>("Value")
.IsRequired();
});
});
b1
.ToJson("FilesConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
{
b1.IsRequired();
b1.Property<string>("DockerImage")
.IsRequired();
b1.Property<string>("Script")
.IsRequired();
b1.Property<string>("Shell")
.IsRequired();
b1
.ToJson("InstallationConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
{
b1.IsRequired();
b1.PrimitiveCollection<string>("OnlineLogPatterns")
.IsRequired();
b1.Property<string>("StopCommand")
.IsRequired();
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
{
b2.IsRequired();
b2.Property<string>("Command")
.IsRequired();
b2.Property<string>("DisplayName")
.IsRequired();
});
b1
.ToJson("LifecycleConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
{
b1.IsRequired();
b1.Property<bool>("UseLegacyStartup");
b1
.ToJson("MiscellaneousConfig")
.HasColumnType("jsonb");
});
b.HasKey("Id");
b.HasIndex("DefaultDockerImageId")
.IsUnique();
b.ToTable("Templates", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("ImageName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("SkipPulling")
.HasColumnType("boolean");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.ToTable("TemplateDockerImages", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("DefaultValue")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("EnvName")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.ToTable("TemplateVariablesVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
.WithOne()
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DefaultDockerImage");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
.WithMany("DockerImages")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
.WithMany("Variables")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,139 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
{
/// <inheritdoc />
public partial class AddedTemplateEntities : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TemplateDockerImages",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
DisplayName = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
ImageName = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
SkipPulling = table.Column<bool>(type: "boolean", nullable: false),
TemplateId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TemplateDockerImages", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Templates",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Description = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
Author = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Version = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
UpdateUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
DonateUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
AllowUserDockerImageChange = table.Column<bool>(type: "boolean", nullable: false),
DefaultDockerImageId = table.Column<int>(type: "integer", nullable: true),
FilesConfig = table.Column<string>(type: "jsonb", nullable: false),
InstallationConfig = table.Column<string>(type: "jsonb", nullable: false),
LifecycleConfig = table.Column<string>(type: "jsonb", nullable: false),
MiscellaneousConfig = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Templates", x => x.Id);
table.ForeignKey(
name: "FK_Templates_TemplateDockerImages_DefaultDockerImageId",
column: x => x.DefaultDockerImageId,
principalSchema: "servers",
principalTable: "TemplateDockerImages",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "TemplateVariablesVariables",
schema: "servers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
DisplayName = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Description = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
EnvName = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
DefaultValue = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
TemplateId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TemplateVariablesVariables", x => x.Id);
table.ForeignKey(
name: "FK_TemplateVariablesVariables_Templates_TemplateId",
column: x => x.TemplateId,
principalSchema: "servers",
principalTable: "Templates",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_TemplateDockerImages_TemplateId",
schema: "servers",
table: "TemplateDockerImages",
column: "TemplateId");
migrationBuilder.CreateIndex(
name: "IX_Templates_DefaultDockerImageId",
schema: "servers",
table: "Templates",
column: "DefaultDockerImageId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_TemplateVariablesVariables_TemplateId",
schema: "servers",
table: "TemplateVariablesVariables",
column: "TemplateId");
migrationBuilder.AddForeignKey(
name: "FK_TemplateDockerImages_Templates_TemplateId",
schema: "servers",
table: "TemplateDockerImages",
column: "TemplateId",
principalSchema: "servers",
principalTable: "Templates",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TemplateDockerImages_Templates_TemplateId",
schema: "servers",
table: "TemplateDockerImages");
migrationBuilder.DropTable(
name: "TemplateVariablesVariables",
schema: "servers");
migrationBuilder.DropTable(
name: "Templates",
schema: "servers");
migrationBuilder.DropTable(
name: "TemplateDockerImages",
schema: "servers");
}
}
}

View File

@@ -0,0 +1,313 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20260312153948_AddedNullabilityForTemplateVariableDefaultValue")]
partial class AddedNullabilityForTemplateVariableDefaultValue
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("HttpEndpointUrl")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Nodes", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowUserDockerImageChange")
.HasColumnType("boolean");
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int?>("DefaultDockerImageId")
.HasColumnType("integer");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("DonateUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("UpdateUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
{
b1.IsRequired();
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
{
b2.IsRequired();
b2.Property<string>("Parser")
.IsRequired();
b2.Property<string>("Path")
.IsRequired();
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
{
b3.IsRequired();
b3.Property<string>("Key")
.IsRequired();
b3.Property<string>("Value");
});
});
b1
.ToJson("FilesConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
{
b1.IsRequired();
b1.Property<string>("DockerImage")
.IsRequired();
b1.Property<string>("Script")
.IsRequired();
b1.Property<string>("Shell")
.IsRequired();
b1
.ToJson("InstallationConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
{
b1.IsRequired();
b1.PrimitiveCollection<string>("OnlineLogPatterns")
.IsRequired();
b1.Property<string>("StopCommand")
.IsRequired();
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
{
b2.IsRequired();
b2.Property<string>("Command")
.IsRequired();
b2.Property<string>("DisplayName")
.IsRequired();
});
b1
.ToJson("LifecycleConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
{
b1.IsRequired();
b1.Property<bool>("UseLegacyStartup");
b1
.ToJson("MiscellaneousConfig")
.HasColumnType("jsonb");
});
b.HasKey("Id");
b.HasIndex("DefaultDockerImageId")
.IsUnique();
b.ToTable("Templates", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("ImageName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("SkipPulling")
.HasColumnType("boolean");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.ToTable("TemplateDockerImages", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("DefaultValue")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("EnvName")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.ToTable("TemplateVariablesVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
.WithOne()
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DefaultDockerImage");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
.WithMany("DockerImages")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
.WithMany("Variables")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

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

View File

@@ -0,0 +1,310 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MoonlightServers.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
{
[DbContext(typeof(DataContext))]
partial class DataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("servers")
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("HttpEndpointUrl")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Nodes", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowUserDockerImageChange")
.HasColumnType("boolean");
b.Property<string>("Author")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int?>("DefaultDockerImageId")
.HasColumnType("integer");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("DonateUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("UpdateUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
{
b1.IsRequired();
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
{
b2.IsRequired();
b2.Property<string>("Parser")
.IsRequired();
b2.Property<string>("Path")
.IsRequired();
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
{
b3.IsRequired();
b3.Property<string>("Key")
.IsRequired();
b3.Property<string>("Value");
});
});
b1
.ToJson("FilesConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
{
b1.IsRequired();
b1.Property<string>("DockerImage")
.IsRequired();
b1.Property<string>("Script")
.IsRequired();
b1.Property<string>("Shell")
.IsRequired();
b1
.ToJson("InstallationConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
{
b1.IsRequired();
b1.PrimitiveCollection<string>("OnlineLogPatterns")
.IsRequired();
b1.Property<string>("StopCommand")
.IsRequired();
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
{
b2.IsRequired();
b2.Property<string>("Command")
.IsRequired();
b2.Property<string>("DisplayName")
.IsRequired();
});
b1
.ToJson("LifecycleConfig")
.HasColumnType("jsonb");
});
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
{
b1.IsRequired();
b1.Property<bool>("UseLegacyStartup");
b1
.ToJson("MiscellaneousConfig")
.HasColumnType("jsonb");
});
b.HasKey("Id");
b.HasIndex("DefaultDockerImageId")
.IsUnique();
b.ToTable("Templates", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("ImageName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("SkipPulling")
.HasColumnType("boolean");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.ToTable("TemplateDockerImages", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("DefaultValue")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("EnvName")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.ToTable("TemplateVariablesVariables", "servers");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
.WithOne()
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DefaultDockerImage");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
.WithMany("DockerImages")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
{
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
.WithMany("Variables")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
});
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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,25 +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; }
// Misc
public bool EnableTransparentMode { get; set; }
public bool EnableDynamicFirewall { get; set; }
}

View File

@@ -1,29 +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; }
public bool UseVirtualDisk { get; set; }
public int Bandwidth { 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,452 +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("20250226210232_RecreatedMigrationsForPostgresql")]
partial class RecreatedMigrationsForPostgresql
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.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("Servers_Allocations", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("EnableDynamicFirewall")
.HasColumnType("boolean");
b.Property<bool>("EnableTransparentMode")
.HasColumnType("boolean");
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<bool>("UseSsl")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("Servers_Nodes", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Bandwidth")
.HasColumnType("integer");
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.Property<bool>("UseVirtualDisk")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers_Servers", (string)null);
});
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("Servers_ServerBackups", (string)null);
});
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("Servers_ServerVariables", (string)null);
});
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("Servers_Stars", (string)null);
});
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("Servers_StarDockerImages", (string)null);
});
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("Servers_StarVariables", (string)null);
});
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.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("Variables");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,286 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class RecreatedMigrationsForPostgresql : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Servers_Nodes",
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),
HttpPort = table.Column<int>(type: "integer", nullable: false),
FtpPort = table.Column<int>(type: "integer", nullable: false),
UseSsl = table.Column<bool>(type: "boolean", nullable: false),
EnableTransparentMode = table.Column<bool>(type: "boolean", nullable: false),
EnableDynamicFirewall = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_Nodes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Servers_Stars",
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_Servers_Stars", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Servers_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),
UseVirtualDisk = table.Column<bool>(type: "boolean", nullable: false),
Bandwidth = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Servers_Servers", x => x.Id);
table.ForeignKey(
name: "FK_Servers_Servers_Servers_Nodes_NodeId",
column: x => x.NodeId,
principalTable: "Servers_Nodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Servers_Servers_Servers_Stars_StarId",
column: x => x.StarId,
principalTable: "Servers_Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Servers_StarDockerImages",
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_Servers_StarDockerImages", x => x.Id);
table.ForeignKey(
name: "FK_Servers_StarDockerImages_Servers_Stars_StarId",
column: x => x.StarId,
principalTable: "Servers_Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Servers_StarVariables",
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_Servers_StarVariables", x => x.Id);
table.ForeignKey(
name: "FK_Servers_StarVariables_Servers_Stars_StarId",
column: x => x.StarId,
principalTable: "Servers_Stars",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Servers_Allocations",
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_Servers_Allocations", x => x.Id);
table.ForeignKey(
name: "FK_Servers_Allocations_Servers_Nodes_NodeId",
column: x => x.NodeId,
principalTable: "Servers_Nodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Servers_Allocations_Servers_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers_Servers",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Servers_ServerBackups",
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_Servers_ServerBackups", x => x.Id);
table.ForeignKey(
name: "FK_Servers_ServerBackups_Servers_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers_Servers",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Servers_ServerVariables",
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_Servers_ServerVariables", x => x.Id);
table.ForeignKey(
name: "FK_Servers_ServerVariables_Servers_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers_Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Servers_Allocations_NodeId",
table: "Servers_Allocations",
column: "NodeId");
migrationBuilder.CreateIndex(
name: "IX_Servers_Allocations_ServerId",
table: "Servers_Allocations",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_Servers_ServerBackups_ServerId",
table: "Servers_ServerBackups",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_Servers_Servers_NodeId",
table: "Servers_Servers",
column: "NodeId");
migrationBuilder.CreateIndex(
name: "IX_Servers_Servers_StarId",
table: "Servers_Servers",
column: "StarId");
migrationBuilder.CreateIndex(
name: "IX_Servers_ServerVariables_ServerId",
table: "Servers_ServerVariables",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_Servers_StarDockerImages_StarId",
table: "Servers_StarDockerImages",
column: "StarId");
migrationBuilder.CreateIndex(
name: "IX_Servers_StarVariables_StarId",
table: "Servers_StarVariables",
column: "StarId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Servers_Allocations");
migrationBuilder.DropTable(
name: "Servers_ServerBackups");
migrationBuilder.DropTable(
name: "Servers_ServerVariables");
migrationBuilder.DropTable(
name: "Servers_StarDockerImages");
migrationBuilder.DropTable(
name: "Servers_StarVariables");
migrationBuilder.DropTable(
name: "Servers_Servers");
migrationBuilder.DropTable(
name: "Servers_Nodes");
migrationBuilder.DropTable(
name: "Servers_Stars");
}
}
}

View File

@@ -1,456 +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("20250301142415_AddedTokenIdField")]
partial class AddedTokenIdField
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.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("Servers_Allocations", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("EnableDynamicFirewall")
.HasColumnType("boolean");
b.Property<bool>("EnableTransparentMode")
.HasColumnType("boolean");
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("Servers_Nodes", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Bandwidth")
.HasColumnType("integer");
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.Property<bool>("UseVirtualDisk")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers_Servers", (string)null);
});
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("Servers_ServerBackups", (string)null);
});
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("Servers_ServerVariables", (string)null);
});
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("Servers_Stars", (string)null);
});
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("Servers_StarDockerImages", (string)null);
});
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("Servers_StarVariables", (string)null);
});
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.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("Variables");
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b =>
{
b.Navigation("DockerImages");
b.Navigation("Variables");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedTokenIdField : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "TokenId",
table: "Servers_Nodes",
type: "text",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TokenId",
table: "Servers_Nodes");
}
}
}

View File

@@ -1,540 +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("20250606121013_AddedShares")]
partial class AddedShares
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.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("Servers_Allocations", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("EnableDynamicFirewall")
.HasColumnType("boolean");
b.Property<bool>("EnableTransparentMode")
.HasColumnType("boolean");
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("Servers_Nodes", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Bandwidth")
.HasColumnType("integer");
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.Property<bool>("UseVirtualDisk")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers_Servers", (string)null);
});
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("Servers_ServerBackups", (string)null);
});
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("Servers_ServerShares", (string)null);
});
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("Servers_ServerVariables", (string)null);
});
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("Servers_Stars", (string)null);
});
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("Servers_StarDockerImages", (string)null);
});
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("Servers_StarVariables", (string)null);
});
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("Servers_ServerShares");
b1.ToJson("Content");
b1.WithOwner()
.HasForeignKey("ServerShareId");
b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerSharePermission", "Permissions", b2 =>
{
b2.Property<int>("ServerShareContentServerShareId")
.HasColumnType("integer");
b2.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
b2.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b2.Property<int>("Type")
.HasColumnType("integer");
b2.HasKey("ServerShareContentServerShareId", "__synthesizedOrdinal");
b2.ToTable("Servers_ServerShares");
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,51 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MoonlightServers.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedShares : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Servers_ServerShares",
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_Servers_ServerShares", x => x.Id);
table.ForeignKey(
name: "FK_Servers_ServerShares_Servers_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers_Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Servers_ServerShares_ServerId",
table: "Servers_ServerShares",
column: "ServerId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Servers_ServerShares");
}
}
}

View File

@@ -1,537 +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
.HasAnnotation("ProductVersion", "9.0.5")
.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("Servers_Allocations", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("EnableDynamicFirewall")
.HasColumnType("boolean");
b.Property<bool>("EnableTransparentMode")
.HasColumnType("boolean");
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("Servers_Nodes", (string)null);
});
modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Bandwidth")
.HasColumnType("integer");
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.Property<bool>("UseVirtualDisk")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("NodeId");
b.HasIndex("StarId");
b.ToTable("Servers_Servers", (string)null);
});
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("Servers_ServerBackups", (string)null);
});
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("Servers_ServerShares", (string)null);
});
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("Servers_ServerVariables", (string)null);
});
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("Servers_Stars", (string)null);
});
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("Servers_StarDockerImages", (string)null);
});
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("Servers_StarVariables", (string)null);
});
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("Servers_ServerShares");
b1.ToJson("Content");
b1.WithOwner()
.HasForeignKey("ServerShareId");
b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerSharePermission", "Permissions", b2 =>
{
b2.Property<int>("ServerShareContentServerShareId")
.HasColumnType("integer");
b2.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
b2.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b2.Property<int>("Type")
.HasColumnType("integer");
b2.HasKey("ServerShareContentServerShareId", "__synthesizedOrdinal");
b2.ToTable("Servers_ServerShares");
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,57 +0,0 @@
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.SingleDb;
using Moonlight.ApiServer.Configuration;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
namespace MoonlightServers.ApiServer.Database;
public class ServersDataContext : DatabaseContext
{
public override string Prefix { get; } = "Servers";
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; }
public ServersDataContext(AppConfiguration configuration)
{
Options = new()
{
Host = configuration.Database.Host,
Port = configuration.Database.Port,
Username = configuration.Database.Username,
Password = configuration.Database.Password,
Database = configuration.Database.Database
};
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
#region Shares
modelBuilder.Ignore<ServerShareContent>();
modelBuilder.Ignore<ServerSharePermission>();
modelBuilder.Entity<ServerShare>(builder =>
{
builder.OwnsOne(x => x.Content, navigationBuilder =>
{
navigationBuilder.ToJson();
navigationBuilder.OwnsMany(x => x.Permissions);
});
});
#endregion
}
}

View File

@@ -1,21 +0,0 @@
using MoonlightServers.DaemonShared.Enums;
using MoonlightServers.Shared.Enums;
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,76 +0,0 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
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,239 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
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")]
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("{nodeId:int}/allocations")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<IPagedData<NodeAllocationDetailResponse>> Get(
[FromRoute] int nodeId,
[FromQuery] int page,
[FromQuery] [Range(1, 100)] int pageSize
)
{
var count = await AllocationRepository.Get().CountAsync(x => x.Node.Id == nodeId);
var allocations = await AllocationRepository
.Get()
.Skip(page * pageSize)
.Take(pageSize)
.Where(x => x.Node.Id == nodeId)
.ToArrayAsync();
var mappedAllocations = allocations.Select(x => new NodeAllocationDetailResponse()
{
Id = x.Id,
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray();
return new PagedData<NodeAllocationDetailResponse>()
{
Items = mappedAllocations,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
};
}
[HttpGet("{nodeId:int}/allocations/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<NodeAllocationDetailResponse> GetSingle([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)
throw new HttpApiException("No allocation with that id found", 404);
return new()
{
Id = allocation.Id,
IpAddress = allocation.IpAddress,
Port = allocation.Port
};
}
[HttpPost("{nodeId:int}/allocations")]
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
public async Task<NodeAllocationDetailResponse> Create(
[FromRoute] int nodeId,
[FromBody] CreateNodeAllocationRequest request
)
{
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (node == null)
throw new HttpApiException("No node with that id found", 404);
var allocation = new Allocation
{
IpAddress = request.IpAddress,
Port = request.Port,
Node = node
};
var finalVariable = await AllocationRepository.Add(allocation);
return new()
{
Id = finalVariable.Id,
IpAddress = finalVariable.IpAddress,
Port = finalVariable.Port
};
}
[HttpPatch("{nodeId:int}/allocations/{id:int}")]
public async Task<NodeAllocationDetailResponse> Update([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)
throw new HttpApiException("No allocation with that id found", 404);
allocation.IpAddress = request.IpAddress;
allocation.Port = request.Port;
await AllocationRepository.Update(allocation);
return new()
{
Id = allocation.Id,
IpAddress = allocation.IpAddress,
Port = allocation.Port
};
}
[HttpDelete("{nodeId:int}/allocations/{id:int}")]
public async Task Delete([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)
throw new HttpApiException("No allocation with that id found", 404);
await AllocationRepository.Remove(allocation);
}
[HttpPost("{nodeId:int}/allocations/range")]
public async Task CreateRange([FromRoute] int nodeId, [FromBody] CreateNodeAllocationRangeRequest rangeRequest)
{
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (node == null)
throw new HttpApiException("No node with that id found", 404);
var existingAllocations = AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.ToArray();
var ports = new List<int>();
for (var i = rangeRequest.Start; i < rangeRequest.End; i++)
{
// Skip existing allocations
if (existingAllocations.Any(x => x.Port == i && x.IpAddress == rangeRequest.IpAddress))
continue;
ports.Add(i);
}
var allocations = ports
.Select(port => new Allocation()
{
IpAddress = rangeRequest.IpAddress,
Port = port,
Node = node
})
.ToArray();
await AllocationRepository.RunTransaction(async set => { await set.AddRangeAsync(allocations); });
}
[HttpDelete("{nodeId:int}/allocations/all")]
public async Task DeleteAll([FromRoute] int nodeId)
{
var allocations = AllocationRepository
.Get()
.Where(x => x.Node.Id == nodeId)
.ToArray();
await AllocationRepository.RunTransaction(set => { set.RemoveRange(allocations); });
}
[HttpGet("{nodeId:int}/allocations/free")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<IPagedData<NodeAllocationDetailResponse>> GetFree([FromRoute] int nodeId, [FromQuery] int page,
[FromQuery][Range(1, 100)] int pageSize, [FromQuery] int serverId = -1)
{
var node = NodeRepository
.Get()
.FirstOrDefault(x => x.Id == nodeId);
if (node == null)
throw new HttpApiException("A node with this id could not be found", 404);
var freeAllocationsQuery = AllocationRepository
.Get()
.Where(x => x.Node.Id == node.Id)
.Where(x => x.Server == null || x.Server.Id == serverId);
var count = await freeAllocationsQuery.CountAsync();
var allocations = await freeAllocationsQuery.ToArrayAsync();
var mappedAllocations = allocations.Select(x => new NodeAllocationDetailResponse()
{
Id = x.Id,
IpAddress = x.IpAddress,
Port = x.Port
}).ToArray();
return new PagedData<NodeAllocationDetailResponse>()
{
Items = mappedAllocations,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
};
}
}

View File

@@ -1,79 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using Microsoft.AspNetCore.Authorization;
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}/system/status")]
[Authorize(Policy = "permissions:admin.servers.nodes.status")]
public async Task<NodeSystemStatusResponse> GetStatus([FromRoute] int nodeId)
{
var node = GetNode(nodeId);
NodeSystemStatusResponse response;
var sw = new Stopwatch();
sw.Start();
try
{
var statusResponse = await NodeService.GetSystemStatus(node);
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 Node GetNode(int nodeId)
{
var result = NodeRepository
.Get()
.FirstOrDefault(x => x.Id == nodeId);
if (result == null)
throw new HttpApiException("A node with this id could not be found", 404);
return result;
}
}

View File

@@ -1,72 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.Nodes;
using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations;
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 CrudHelper<Node, NodeDetailResponse> CrudHelper;
private readonly DatabaseRepository<Node> NodeRepository;
public NodesController(
CrudHelper<Node, NodeDetailResponse> crudHelper,
DatabaseRepository<Node> nodeRepository
)
{
CrudHelper = crudHelper;
NodeRepository = nodeRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<IPagedData<NodeDetailResponse>> Get([FromQuery] int page, [FromQuery] int pageSize)
{
return await CrudHelper.Get(page, pageSize);
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
public async Task<NodeDetailResponse> GetSingle([FromRoute] int id)
{
return await CrudHelper.GetSingle(id);
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
public async Task<NodeDetailResponse> Create([FromBody] CreateNodeRequest request)
{
var node = Mapper.Map<Node>(request);
node.TokenId = Formatter.GenerateString(6);
node.Token = Formatter.GenerateString(32);
var finalNode = await NodeRepository.Add(node);
return CrudHelper.MapToResult(finalNode);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.update")]
public async Task<NodeDetailResponse> Update([FromRoute] int id, [FromBody] UpdateNodeRequest request)
{
return await CrudHelper.Update(id, request);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.nodes.delete")]
public async Task Delete([FromRoute] int id)
{
await CrudHelper.Delete(id);
}
}

View File

@@ -1,90 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
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")]
[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("{nodeId:int}/statistics")]
public async Task<StatisticsResponse> Get([FromRoute] int nodeId)
{
var node = await GetNode(nodeId);
var statistics = await NodeService.GetStatistics(node);
return new()
{
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("{nodeId:int}/statistics/docker")]
public async Task<DockerStatisticsResponse> GetDocker([FromRoute] int nodeId)
{
var node = await GetNode(nodeId);
var statistics = await NodeService.GetDockerStatistics(node);
return new()
{
BuildCacheReclaimable = statistics.BuildCacheReclaimable,
BuildCacheUsed = statistics.BuildCacheUsed,
ContainersReclaimable = statistics.ContainersReclaimable,
ContainersUsed = statistics.ContainersUsed,
ImagesReclaimable = statistics.ImagesReclaimable,
ImagesUsed = statistics.ImagesUsed,
Version = statistics.Version
};
}
private async Task<Node> GetNode(int nodeId)
{
var result = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == nodeId);
if (result == null)
throw new HttpApiException("A node with this id could not be found", 404);
return result;
}
}

View File

@@ -1,48 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
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}/variables")]
[Authorize(Policy = "permissions:admin.servers.get")]
public async Task<PagedData<ServerVariableDetailResponse>> Get([FromRoute] int serverId, [FromQuery] int page, [FromQuery] int pageSize)
{
var server = await ServerRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var variables = await VariableRepository
.Get()
.Where(x => x.Server.Id == server.Id)
.ToArrayAsync();
var castedVariables = variables
.Select(x => Mapper.Map<ServerVariableDetailResponse>(x))
.ToArray();
return PagedData<ServerVariableDetailResponse>.Create(castedVariables, page, pageSize);
}
}

View File

@@ -1,282 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Helpers;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
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 CrudHelper<Server, ServerDetailResponse> CrudHelper;
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(
CrudHelper<Server, ServerDetailResponse> crudHelper,
DatabaseRepository<Star> starRepository,
DatabaseRepository<Node> nodeRepository,
DatabaseRepository<Allocation> allocationRepository,
DatabaseRepository<ServerVariable> variableRepository,
DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository,
ILogger<ServersController> logger,
ServerService serverService
)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
NodeRepository = nodeRepository;
AllocationRepository = allocationRepository;
VariableRepository = variableRepository;
ServerRepository = serverRepository;
UserRepository = userRepository;
ServerService = serverService;
Logger = logger;
CrudHelper.QueryModifier = servers => servers
.Include(x => x.Node)
.Include(x => x.Allocations)
.Include(x => x.Variables)
.Include(x => x.Star);
CrudHelper.LateMapper = (server, response) =>
{
response.NodeId = server.Node.Id;
response.StarId = server.Star.Id;
response.AllocationIds = server.Allocations.Select(x => x.Id).ToArray();
return response;
};
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.get")]
public async Task<IPagedData<ServerDetailResponse>> Get([FromQuery] int page, [FromQuery] int pageSize)
{
return await CrudHelper.Get(page, pageSize);
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.get")]
public async Task<ServerDetailResponse> GetSingle([FromRoute] int id)
{
return await CrudHelper.GetSingle(id);
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.create")]
public async Task<ServerDetailResponse> Create([FromBody] CreateServerRequest request)
{
// Construct model
var server = Mapper.Map<Server>(request);
// Check if owner user exist
if (UserRepository.Get().All(x => x.Id != request.OwnerId))
throw new HttpApiException("No user with this id found", 400);
var star = await StarRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.DockerImages)
.FirstOrDefaultAsync(x => x.Id == request.StarId);
if (star == null)
throw new HttpApiException("No star with this id found", 400);
var node = await NodeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == request.NodeId);
if (node == null)
throw new HttpApiException("No node with this id found", 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)
{
throw new HttpApiException(
$"Unable to find enough free allocations. Found: {allocations.Count}, Required: {star.RequiredAllocations}",
400
);
}
}
// 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.Add(server);
try
{
await ServerService.Sync(finalServer);
}
catch (Exception e)
{
Logger.LogError("Unable to sync server to node the server is assigned to: {e}", e);
// We are deleting the server from the database after the creation has failed
// to ensure we wont have a bugged server in the database which doesnt exist on the node
await ServerRepository.Remove(finalServer);
throw;
}
return CrudHelper.MapToResult(finalServer);
}
[HttpPatch("{id:int}")]
public async Task<ServerDetailResponse> Update([FromRoute] int id, [FromBody] UpdateServerRequest request)
{
//TODO: Handle shrinking virtual disk
var server = await CrudHelper.GetSingleModel(id);
server = Mapper.Map(server, request);
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)
{
throw new HttpApiException(
$"You need to specify at least {server.Star.RequiredAllocations} allocation(s)",
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.Update(server);
// Notify the node about the changes
await ServerService.Sync(server);
return CrudHelper.MapToResult(server);
}
[HttpDelete("{id:int}")]
public async Task Delete([FromRoute] int id, [FromQuery] bool force = false)
{
var server = await CrudHelper.GetSingleModel(id);
try
{
// If the sync fails on the node and we aren't forcing the deletion,
// we don't want to delete it from the database yet
await ServerService.SyncDelete(server);
}
catch (Exception e)
{
if (force)
{
Logger.LogWarning(
"An error occured while syncing deletion of a server to the node. Continuing anyways. Error: {e}",
e
);
}
else
throw;
}
await CrudHelper.Delete(id);
}
}

View File

@@ -1,101 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
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")]
public class StarDockerImagesController : Controller
{
private readonly CrudHelper<StarDockerImage, StarDockerImageDetailResponse> CrudHelper;
private readonly DatabaseRepository<Star> StarRepository;
private readonly DatabaseRepository<StarDockerImage> StarDockerImageRepository;
private Star Star;
public StarDockerImagesController(
CrudHelper<StarDockerImage, StarDockerImageDetailResponse> crudHelper,
DatabaseRepository<Star> starRepository,
DatabaseRepository<StarDockerImage> starDockerImageRepository
)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
StarDockerImageRepository = starDockerImageRepository;
}
private async Task ApplyStar(int id)
{
var star = await StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (star == null)
throw new HttpApiException("A star with this id could not be found", 404);
Star = star;
CrudHelper.QueryModifier = dockerImages =>
dockerImages.Where(x => x.Star.Id == star.Id);
}
[HttpGet("{starId:int}/dockerImages")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<IPagedData<StarDockerImageDetailResponse>> Get([FromRoute] int starId, [FromQuery] int page, [FromQuery] int pageSize)
{
await ApplyStar(starId);
return await CrudHelper.Get(page, pageSize);
}
[HttpGet("{starId:int}/dockerImages/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<StarDockerImageDetailResponse> GetSingle([FromRoute] int starId, [FromRoute] int id)
{
await ApplyStar(starId);
return await CrudHelper.GetSingle(id);
}
[HttpPost("{starId:int}/dockerImages")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarDockerImageDetailResponse> Create([FromRoute] int starId, [FromBody] CreateStarDockerImageRequest request)
{
await ApplyStar(starId);
var starDockerImage = Mapper.Map<StarDockerImage>(request);
starDockerImage.Star = Star;
var finalVariable = await StarDockerImageRepository.Add(starDockerImage);
return CrudHelper.MapToResult(finalVariable);
}
[HttpPatch("{starId:int}/dockerImages/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.update")]
public async Task<StarDockerImageDetailResponse> Update([FromRoute] int starId, [FromRoute] int id,
[FromBody] UpdateStarDockerImageRequest request)
{
await ApplyStar(starId);
return await CrudHelper.Update(id, request);
}
[HttpDelete("{starId:int}/dockerImages/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
public async Task Delete([FromRoute] int starId, [FromRoute] int id)
{
await ApplyStar(starId);
await CrudHelper.Delete(id);
}
}

View File

@@ -1,52 +0,0 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Exceptions;
using MoonCore.Helpers;
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 Export([FromRoute] int starId)
{
var exportedStar = await ImportExportService.Export(starId);
Response.StatusCode = 200;
Response.ContentType = "application/json";
await Response.WriteAsync(exportedStar);
}
[HttpPost("import")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarDetailResponse> Import()
{
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.Import(content);
return Mapper.Map<StarDetailResponse>(star);
}
}

View File

@@ -1,106 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
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")]
public class StarVariablesController : Controller
{
private readonly CrudHelper<StarVariable, StarVariableDetailResponse> CrudHelper;
private readonly DatabaseRepository<Star> StarRepository;
private readonly DatabaseRepository<StarVariable> StarVariableRepository;
private Star Star;
public StarVariablesController(
CrudHelper<StarVariable, StarVariableDetailResponse> crudHelper,
DatabaseRepository<Star> starRepository,
DatabaseRepository<StarVariable> starVariableRepository)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
StarVariableRepository = starVariableRepository;
}
private async Task ApplyStar(int id)
{
var star = await StarRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (star == null)
throw new HttpApiException("A star with this id could not be found", 404);
Star = star;
CrudHelper.QueryModifier = variables =>
variables.Where(x => x.Star.Id == star.Id);
}
[HttpGet("{starId:int}/variables")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<IPagedData<StarVariableDetailResponse>> Get([FromRoute] int starId, [FromQuery] int page, [FromQuery] int pageSize)
{
await ApplyStar(starId);
return await CrudHelper.Get(page, pageSize);
}
[HttpGet("{starId:int}/variables/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<StarVariableDetailResponse> GetSingle([FromRoute] int starId, [FromRoute] int id)
{
await ApplyStar(starId);
return await CrudHelper.GetSingle(id);
}
[HttpPost("{starId:int}/variables")]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarVariableDetailResponse> Create([FromRoute] int starId, [FromBody] CreateStarVariableRequest request)
{
await ApplyStar(starId);
var starVariable = Mapper.Map<StarVariable>(request);
starVariable.Star = Star;
var finalVariable = await StarVariableRepository.Add(starVariable);
return CrudHelper.MapToResult(finalVariable);
}
[HttpPatch("{starId:int}/variables/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.update")]
public async Task<StarVariableDetailResponse> Update([FromRoute] int starId, [FromRoute] int id,
[FromBody] UpdateStarVariableRequest request)
{
await ApplyStar(starId);
var variable = await CrudHelper.GetSingleModel(id);
variable = Mapper.Map(variable, request);
await StarVariableRepository.Update(variable);
return CrudHelper.MapToResult(variable);
}
[HttpDelete("{starId:int}/variables/{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
public async Task Delete([FromRoute] int starId, [FromRoute] int id)
{
await ApplyStar(starId);
await CrudHelper.Delete(id);
}
}

View File

@@ -1,82 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.Shared.Http.Requests.Admin.Stars;
using MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages;
using MoonlightServers.Shared.Http.Responses.Admin.Stars;
using MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars;
[ApiController]
[Route("api/admin/servers/stars")]
public class StarsController : Controller
{
private readonly CrudHelper<Star, StarDetailResponse> CrudHelper;
private readonly DatabaseRepository<Star> StarRepository;
public StarsController(CrudHelper<Star, StarDetailResponse> crudHelper, DatabaseRepository<Star> starRepository)
{
CrudHelper = crudHelper;
StarRepository = starRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<IPagedData<StarDetailResponse>> Get([FromQuery] int page, [FromQuery] int pageSize)
{
return await CrudHelper.Get(page, pageSize);
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.get")]
public async Task<StarDetailResponse> GetSingle([FromRoute] int id)
{
return await CrudHelper.GetSingle(id);
}
[HttpPost]
[Authorize(Policy = "permissions:admin.servers.stars.create")]
public async Task<StarDetailResponse> Create([FromBody] CreateStarRequest request)
{
var star = Mapper.Map<Star>(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.Add(star);
return CrudHelper.MapToResult(finalStar);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.update")]
public async Task<StarDetailResponse> Update([FromRoute] int id, [FromBody] UpdateStarRequest request)
{
return await CrudHelper.Update(id, request);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
public async Task Delete([FromRoute] int id)
{
await CrudHelper.Delete(id);
}
}

View File

@@ -1,182 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.DaemonShared.Enums;
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")]
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("{serverId:int}/files/list")]
public async Task<ServerFilesEntryResponse[]> List([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.Read);
var entries = await ServerFileSystemService.List(server, path);
return entries.Select(x => new ServerFilesEntryResponse()
{
Name = x.Name,
Size = x.Size,
IsFile = x.IsFile,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
}).ToArray();
}
[HttpPost("{serverId:int}/files/move")]
public async Task Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
await ServerFileSystemService.Move(server, oldPath, newPath);
}
[HttpDelete("{serverId:int}/files/delete")]
public async Task Delete([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
await ServerFileSystemService.Delete(server, path);
}
[HttpPost("{serverId:int}/files/mkdir")]
public async Task Mkdir([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
await ServerFileSystemService.Mkdir(server, path);
}
[HttpGet("{serverId:int}/files/upload")]
public async Task<ServerFilesUploadResponse> Upload([FromRoute] int serverId)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
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("{serverId:int}/files/download")]
public async Task<ServerFilesDownloadResponse> Download([FromRoute] int serverId, [FromQuery] string path)
{
var server = await GetServerById(serverId, ServerPermissionType.Read);
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("{serverId:int}/files/compress")]
public async Task Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
if (!Enum.TryParse(request.Type, true, out CompressType type))
throw new HttpApiException("Invalid compress type provided", 400);
await ServerFileSystemService.Compress(server, type, request.Items, request.Destination);
}
[HttpPost("{serverId:int}/files/decompress")]
public async Task Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
if (!Enum.TryParse(request.Type, true, out CompressType type))
throw new HttpApiException("Invalid compress type provided", 400);
await ServerFileSystemService.Decompress(server, type, request.Path, request.Destination);
}
private async Task<Server> GetServerById(int serverId, ServerPermissionType type)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var authorizeResult = await AuthorizeService.Authorize(
User, server,
permission => permission.Name == "files" && permission.Type >= type
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
authorizeResult.Message ?? "No permission for the requested resource",
403
);
}
return server;
}
}

View File

@@ -1,86 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Enums;
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
[ApiController]
[Authorize]
[Route("api/client/servers")]
public class PowerController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly DatabaseRepository<User> UserRepository;
private readonly ServerService ServerService;
private readonly ServerAuthorizeService AuthorizeService;
public PowerController(
DatabaseRepository<Server> serverRepository,
DatabaseRepository<User> userRepository,
ServerService serverService,
ServerAuthorizeService authorizeService
)
{
ServerRepository = serverRepository;
UserRepository = userRepository;
ServerService = serverService;
AuthorizeService = authorizeService;
}
[HttpPost("{serverId:int}/start")]
[Authorize]
public async Task Start([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Start(server);
}
[HttpPost("{serverId:int}/stop")]
[Authorize]
public async Task Stop([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Stop(server);
}
[HttpPost("{serverId:int}/kill")]
[Authorize]
public async Task Kill([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Kill(server);
}
private async Task<Server> GetServerById(int serverId)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var authorizeResult = await AuthorizeService.Authorize(
User, server,
permission => permission.Name == "power" && permission.Type >= ServerPermissionType.ReadWrite
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
authorizeResult.Message ?? "No permission for the requested resource",
403
);
}
return server;
}
}

View File

@@ -1,318 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Extensions;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Responses.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Allocations;
using MoonlightServers.Shared.Models;
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<PagedData<ServerDetailResponse>> GetAll([FromQuery] int page, [FromQuery] int pageSize)
{
var userIdClaim = User.FindFirstValue("userId");
if (string.IsNullOrEmpty(userIdClaim))
throw new HttpApiException("Only users are able to use this endpoint", 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 count = await query.CountAsync();
var items = await query.Skip(page * pageSize).Take(pageSize).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 PagedData<ServerDetailResponse>()
{
Items = mappedItems,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : count / pageSize
};
}
[HttpGet("shared")]
public async Task<PagedData<ServerDetailResponse>> GetAllShared([FromQuery] int page, [FromQuery] int pageSize)
{
var userIdClaim = User.FindFirstValue("userId");
if (string.IsNullOrEmpty(userIdClaim))
throw new HttpApiException("Only users are able to use this endpoint", 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 count = await query.CountAsync();
var items = await query.Skip(page * pageSize).Take(pageSize).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 = x.Content.Permissions.ToArray()
}
}).ToArray();
return new PagedData<ServerDetailResponse>()
{
Items = mappedItems,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : count / pageSize
};
}
[HttpGet("{serverId:int}")]
public async Task<ServerDetailResponse> Get([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)
throw new HttpApiException("No server with this id found", 404);
var authorizationResult = await AuthorizeService.Authorize(User, server);
if (!authorizationResult.Succeeded)
{
throw new HttpApiException(
authorizationResult.Message ?? "No server with this id found",
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 = authorizationResult.Share.Content.Permissions.ToArray()
};
}
return response;
}
[HttpGet("{serverId:int}/status")]
public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
var status = await ServerService.GetStatus(server);
return new ServerStatusResponse()
{
State = status.State.ToServerPowerState()
};
}
[HttpGet("{serverId:int}/ws")]
public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId)
{
var server = await GetServerById(
serverId,
permission => permission is { Name: "console", Type: >= ServerPermissionType.Read }
);
// 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<ServerLogsResponse> GetLogs([FromRoute] int serverId)
{
var server = await GetServerById(
serverId,
permission => permission is { Name: "console", Type: >= ServerPermissionType.Read }
);
var logs = await ServerService.GetLogs(server);
return new ServerLogsResponse()
{
Messages = logs.Messages
};
}
[HttpGet("{serverId:int}/stats")]
public async Task<ServerStatsResponse> GetStats([FromRoute] int serverId)
{
var server = await GetServerById(
serverId
);
var stats = await ServerService.GetStats(server);
return new ServerStatsResponse()
{
CpuUsage = stats.CpuUsage,
MemoryUsage = stats.MemoryUsage,
NetworkRead = stats.NetworkRead,
NetworkWrite = stats.NetworkWrite,
IoRead = stats.IoRead,
IoWrite = stats.IoWrite
};
}
private async Task<Server> GetServerById(int serverId, Func<ServerSharePermission, bool>? filter = null)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var authorizeResult = await AuthorizeService.Authorize(User, server, filter);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
authorizeResult.Message ?? "No permission for the requested resource",
403
);
}
return server;
}
}

View File

@@ -1,65 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
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 Install([FromRoute] int serverId)
{
var server = await GetServerById(serverId);
await ServerService.Install(server);
}
private async Task<Server> GetServerById(int serverId)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var authorizeResult = await AuthorizeService.Authorize(
User, server,
permission => permission is { Name: "settings", Type: >= ServerPermissionType.ReadWrite }
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
authorizeResult.Message ?? "No permission for the requested resource",
403
);
}
return server;
}
}

View File

@@ -1,232 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
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")]
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("{serverId:int}/shares")]
public async Task<PagedData<ServerShareResponse>> GetAll(
[FromRoute] int serverId,
[FromQuery] [Range(0, int.MaxValue)] int page,
[FromQuery] [Range(1, 100)] int pageSize
)
{
var server = await GetServerById(serverId);
var query = ShareRepository
.Get()
.Where(x => x.Server.Id == server.Id);
var count = await query.CountAsync();
var items = await query.Skip(page * pageSize).Take(pageSize).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 = x.Content.Permissions.ToArray()
}).ToArray();
return new PagedData<ServerShareResponse>()
{
Items = mappedItems,
CurrentPage = page,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : count / pageSize
};
}
[HttpGet("{serverId:int}/shares/{id:int}")]
public async Task<ServerShareResponse> Get(
[FromRoute] int serverId,
[FromRoute] int id
)
{
var server = await GetServerById(serverId);
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id);
if (share == null)
throw new HttpApiException("A share with that id cannot be found", 404);
var user = await UserRepository
.Get()
.FirstAsync(x => x.Id == share.UserId);
var mappedItem = new ServerShareResponse()
{
Id = share.Id,
Username = user.Username,
Permissions = share.Content.Permissions.ToArray()
};
return mappedItem;
}
[HttpPost("{serverId:int}/shares")]
public async Task<ServerShareResponse> Create(
[FromRoute] int serverId,
[FromBody] CreateShareRequest request
)
{
var server = await GetServerById(serverId);
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Username == request.Username);
if (user == null)
throw new HttpApiException("A user with that username could not be found", 400);
var share = new ServerShare()
{
Server = server,
Content = new()
{
Permissions = request.Permissions
},
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
UserId = user.Id
};
var finalShare = await ShareRepository.Add(share);
var mappedItem = new ServerShareResponse()
{
Id = finalShare.Id,
Username = user.Username,
Permissions = finalShare.Content.Permissions.ToArray()
};
return mappedItem;
}
[HttpPatch("{serverId:int}/shares/{id:int}")]
public async Task<ServerShareResponse> Update(
[FromRoute] int serverId,
[FromRoute] int id,
[FromBody] UpdateShareRequest request
)
{
var server = await GetServerById(serverId);
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id);
if (share == null)
throw new HttpApiException("A share with that id cannot be found", 404);
share.Content.Permissions = request.Permissions;
share.UpdatedAt = DateTime.UtcNow;
await ShareRepository.Update(share);
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == share.UserId);
if (user == null)
throw new HttpApiException("A user with that id could not be found", 400);
var mappedItem = new ServerShareResponse()
{
Id = share.Id,
Username = user.Username,
Permissions = share.Content.Permissions.ToArray()
};
return mappedItem;
}
[HttpDelete("{serverId:int}/shares/{id:int}")]
public async Task Delete(
[FromRoute] int serverId,
[FromRoute] int id
)
{
var server = await GetServerById(serverId);
var share = await ShareRepository
.Get()
.FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id);
if (share == null)
throw new HttpApiException("A share with that id cannot be found", 404);
await ShareRepository.Remove(share);
}
private async Task<Server> GetServerById(int serverId)
{
var server = await ServerRepository
.Get()
.Include(x => x.Node)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var authorizeResult = await AuthorizeService.Authorize(
User, server,
permission => permission is { Name: "shares", Type: >= ServerPermissionType.ReadWrite }
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
authorizeResult.Message ?? "No permission for the requested resource",
403
);
}
return server;
}
}

View File

@@ -1,150 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Services;
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")]
public class VariablesController : Controller
{
private readonly DatabaseRepository<Server> ServerRepository;
private readonly ServerAuthorizeService AuthorizeService;
public VariablesController(
DatabaseRepository<Server> serverRepository,
ServerAuthorizeService authorizeService
)
{
ServerRepository = serverRepository;
AuthorizeService = authorizeService;
}
[HttpGet("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse[]> Get([FromRoute] int serverId)
{
var server = await GetServerById(serverId, ServerPermissionType.Read);
return server.Star.Variables.Select(starVariable =>
{
var serverVariable = server.Variables.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();
}
[HttpPut("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse> UpdateSingle(
[FromRoute] int serverId,
[FromBody] UpdateServerVariableRequest request
)
{
// TODO: Handle filter
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
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.Update(server);
return new ServerVariableDetailResponse()
{
Key = starVariable.Key,
Value = serverVariable.Value,
Type = starVariable.Type,
Name = starVariable.Name,
Description = starVariable.Description,
Filter = starVariable.Filter
};
}
[HttpPatch("{serverId:int}/variables")]
public async Task<ServerVariableDetailResponse[]> Update(
[FromRoute] int serverId,
[FromBody] UpdateServerVariableRangeRequest request
)
{
var server = await GetServerById(serverId, ServerPermissionType.ReadWrite);
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.Update(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<Server> GetServerById(int serverId, ServerPermissionType type)
{
var server = await ServerRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.Star)
.ThenInclude(x => x.Variables)
.FirstOrDefaultAsync(x => x.Id == serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var authorizeResult = await AuthorizeService.Authorize(
User, server,
permission => permission.Name == "variables" && permission.Type >= type
);
if (!authorizeResult.Succeeded)
{
throw new HttpApiException(
authorizeResult.Message ?? "No permission for the requested resource",
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 Get() => Task.CompletedTask;
}

View File

@@ -1,194 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
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<PagedData<ServerDataResponse>> Get([FromQuery] int page, [FromQuery] int pageSize)
{
// 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(page * pageSize)
.Take(pageSize)
.ToArrayAsync();
var serverData = new List<ServerDataResponse>();
foreach (var server in servers)
{
var convertedData = ConvertToServerData(server);
if (convertedData == null)
continue;
serverData.Add(convertedData);
}
return new PagedData<ServerDataResponse>()
{
Items = serverData.ToArray(),
CurrentPage = page,
PageSize = pageSize,
TotalItems = total,
TotalPages = total == 0 ? 0 : total / pageSize
};
}
[HttpGet("{id:int}")]
public async Task<ServerDataResponse> Get([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> GetInstall([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),
Bandwidth = server.Bandwidth,
Cpu = server.Cpu,
Disk = server.Disk,
Memory = server.Memory,
OnlineDetection = server.Star.OnlineDetection,
DockerImage = dockerImage.Identifier,
PullDockerImage = dockerImage.AutoPulling,
ParseConiguration = server.Star.ParseConfiguration,
StopCommand = server.Star.StopCommand,
UseVirtualDisk = server.UseVirtualDisk
};
}
}

View File

@@ -1,33 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using MoonCore.Attributes;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
public class AdminAuthFilter : IServerAuthorizationFilter
{
private readonly IAuthorizationService AuthorizationService;
public AdminAuthFilter(IAuthorizationService authorizationService)
{
AuthorizationService = authorizationService;
}
public async Task<ServerAuthorizationResult?> Process(
ClaimsPrincipal user,
Server server,
Func<ServerSharePermission, bool>? filter = null
)
{
var authResult = await AuthorizationService.AuthorizeAsync(
user,
"permissions:admin.servers.manage"
);
return authResult.Succeeded ? ServerAuthorizationResult.Success() : null;
}
}

View File

@@ -1,28 +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.Models;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
public class OwnerAuthFilter : IServerAuthorizationFilter
{
public Task<ServerAuthorizationResult?> Process(ClaimsPrincipal user, Server server, Func<ServerSharePermission, bool>? filter = null)
{
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,49 +0,0 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using MoonCore.Attributes;
using MoonCore.Extended.Abstractions;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Interfaces;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters;
public class ShareAuthFilter : IServerAuthorizationFilter
{
private readonly DatabaseRepository<ServerShare> ShareRepository;
public ShareAuthFilter(DatabaseRepository<ServerShare> shareRepository)
{
ShareRepository = shareRepository;
}
public async Task<ServerAuthorizationResult?> Process(
ClaimsPrincipal user,
Server server,
Func<ServerSharePermission, bool>? filter = null
)
{
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(filter == null)
return ServerAuthorizationResult.Success(share);
if(share.Content.Permissions.Any(filter))
return ServerAuthorizationResult.Success(share);
return null;
}
}

View File

@@ -1,17 +0,0 @@
using System.Security.Claims;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.Shared.Models;
namespace MoonlightServers.ApiServer.Interfaces;
public interface IServerAuthorizationFilter
{
// Return null => skip to next filter / handler
// Return any value, instant return
public Task<ServerAuthorizationResult?> Process(
ClaimsPrincipal user,
Server server,
Func<ServerSharePermission, bool>? filter = null
);
}

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,8 +0,0 @@
using MoonlightServers.Shared.Models;
namespace MoonlightServers.ApiServer.Models;
public class ServerShareContent
{
public List<ServerSharePermission> Permissions { 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; }
}
}

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