diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..928e628f
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+**/appsettings.json
+**/appsettings.Development.json
+**/appsettings*
+**/compose.*
+**/.env.example
+LICENSE
+README.md
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..c80469a5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,10 @@
+DATABASE_HOST=localhost
+DATABASE_PORT=5432
+DATABASE_USERNAME=user
+DATABASE_PASSWORD=user
+DATABASE_DATABASE=user
+OIDC_AUTHORITY=http://localhost:8092
+OIDC_AUTHORITY=http://localhost:8092
+OIDC_CLIENT_ID=client_id
+OIDC_CLIENT_SECRET=client_secret
+OIDC_REQUIRE_HTTPS_METADATA=false
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index dfe07704..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1,2 +0,0 @@
-# Auto detect text files and perform LF normalization
-* text=auto
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 997f071a..00000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-# These are supported funding model platforms
-ko_fi: masuowo
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
deleted file mode 100644
index 7334cb5d..00000000
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-name: Bug Report
-description: Something isn't working quite right in the software.
-labels: [not confirmed]
-body:
-- type: markdown
- attributes:
- value: |
- Bug reports should only be used for reporting issues with how the software works. For assistance installing this software, as well as debugging issues with dependencies, please use our [Discord server](https://discord.gg/TJaspT7A8p).
-
-- type: textarea
- attributes:
- label: Current Behavior
- description: Please provide a clear & concise description of the issue.
- validations:
- required: true
-
-- type: textarea
- attributes:
- label: Expected Behavior
- description: Please describe what you expected to happen.
- validations:
- required: true
-
-- type: textarea
- attributes:
- label: Steps to Reproduce
- description: Please be as detailed as possible when providing steps to reproduce, failure to provide steps will result in this issue being closed.
- validations:
- required: true
-
-- type: input
- id: panel-version
- attributes:
- label: Panel Version
- description: Version number of your Panel (latest is not a version)
- placeholder: v2 EA
- validations:
- required: true
-
-- type: input
- id: daemon-version
- attributes:
- label: Daemon Version
- description: Version number of your Daemon (latest is not a version)
- placeholder: v2 EA
- validations:
- required: true
-
-- type: input
- id: image-details
- attributes:
- label: Games and/or Images Affected
- description: Please include the specific game(s) or Image(s) you are running into this bug with.
- placeholder: Minecraft (Paper), Minecraft (Forge)
-
-- type: input
- id: docker-image
- attributes:
- label: Docker Image
- description: The specific Docker image you are using for the game(s) above.
- placeholder: ghcr.io/xxx/yolks:java_17
-
-- type: textarea
- id: panel-logs
- attributes:
- label: Error Logs
- description: |
- Run the following command to collect logs on your system.
-
- Panel: `docker logs moonlight`
- Wings: `sudo wings diagnostics`
- placeholder: logs here
- render: bash
- validations:
- required: false
-
-- type: checkboxes
- attributes:
- label: Is there an existing issue for this?
- description: Please [search here](https://github.com/Moonlight-Panel/Moonlight/issues) to see if an issue already exists for your problem.
- options:
- - label: I have searched the existing issues before opening this issue.
- required: true
- - label: I have provided all relevant details, including the specific game and Docker images I am using if this issue is related to running a server.
- required: true
- - label: I have checked in the Discord server and believe this is a bug with the software, and not a configuration issue with my specific system.
- required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
deleted file mode 100644
index 7917ea02..00000000
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-blank_issues_enabled: true
-contact_links:
- - name: Installation Help
- url: https://discord.gg/TJaspT7A8p
- about: Please visit our Discord for help with your installation.
- - name: General Question
- url: https://discord.gg/TJaspT7A8p
- about: Please visit our Discord for general questions about Moonlight Panel.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
deleted file mode 100644
index 87c299e5..00000000
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Feature Request
-description: Suggest a new feature or improvement for the software.
-labels: [feature request]
-body:
-- type: checkboxes
- attributes:
- label: Is there an existing feature request for this?
- description: Please [search here](https://github.com/Moonlight-Panel/Moonlight/issues?q=is%3Aissue) to see if someone else has already suggested this.
- options:
- - label: I have searched the existing issues before opening this feature request.
- required: true
-
-- type: textarea
- attributes:
- label: Describe the feature you would like to see.
- description: "A clear & concise description of the feature you'd like to have added, and what issues it would solve."
- validations:
- required: true
-
-- type: textarea
- attributes:
- label: Describe the solution you'd like.
- description: "You must explain how you'd like to see this feature implemented. Technical implementation details are not necessary, rather an idea of how you'd like to see this feature used."
- validations:
- required: true
-
-- type: textarea
- attributes:
- label: Additional context to this request.
- description: "Add any other context or screenshots about the feature request."
- validations:
- required: false
\ No newline at end of file
diff --git a/.github/workflows/publish-dev-packages.yml b/.github/workflows/publish-dev-packages.yml
deleted file mode 100644
index f42e3983..00000000
--- a/.github/workflows/publish-dev-packages.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: Build and Publish NuGet Package
-
-on:
- push:
- branches: [ v2_ChangeArchitecture,v2.1 ]
- paths:
- - '**.csproj'
-
- workflow_dispatch:
-
-jobs:
- publish:
- runs-on: debian-12
- strategy:
- matrix:
- project:
- - Moonlight.Client
- - Moonlight.ApiServer
- - Moonlight.Shared
-
- steps:
- # Step 1: Clean environment
- - name: Clean up Environment
- run: |
- rm -rf ./*
- rm -rf ./.??*
-
- # Step 2: Checkout the code
- - name: Checkout code
- uses: actions/checkout@v3
-
- # Step 3: Build project
- - name: "Build project"
- run: dotnet build --configuration Debug ${{ matrix.project }}/${{ matrix.project }}.csproj
-
- # Step 4: Pack project
- - name: "Pack project"
- run: dotnet pack --configuration Debug --no-build --output . ${{ matrix.project }}/${{ matrix.project }}.csproj
-
- # Step 5: Publish on package registry
- - name: Publish on package registry"
- run: dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{secrets.GH_PACKAGES_READWRITE}} --source https://nuget.pkg.github.com/Moonlight-Panel/index.json
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 57505a98..2678919b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-## Ignore Visual Studio temporary files, build results, and
+## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
@@ -395,40 +395,13 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
-*.sln.iml
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Moonlight
-storage/
**/.idea/**
-style.min.css
-# Build script for nuget packages
-finalPackages/
-nupkgs/
+# Style builds
+**/style.min.css
+**/package-lock.json
-# Scripts
-**/bin/**
-**/obj/**
\ No newline at end of file
+# Secrets
+**/.env
+**/appsettings.json
+**/appsettings.Development.json
\ No newline at end of file
diff --git a/Hosts/Moonlight.Api.Host/AppStartupLoader.cs b/Hosts/Moonlight.Api.Host/AppStartupLoader.cs
new file mode 100644
index 00000000..c1e7a9e8
--- /dev/null
+++ b/Hosts/Moonlight.Api.Host/AppStartupLoader.cs
@@ -0,0 +1,10 @@
+using MoonCore.PluginFramework;
+using Moonlight.Api.Startup;
+
+namespace Moonlight.Api.Host;
+
+[PluginLoader]
+public partial class AppStartupLoader : IAppStartup
+{
+
+}
\ No newline at end of file
diff --git a/Hosts/Moonlight.Api.Host/Dockerfile b/Hosts/Moonlight.Api.Host/Dockerfile
new file mode 100644
index 00000000..36ee6a41
--- /dev/null
+++ b/Hosts/Moonlight.Api.Host/Dockerfile
@@ -0,0 +1,63 @@
+# Base image
+FROM cgr.dev/chainguard/aspnet-runtime:latest AS base
+WORKDIR /app
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+
+# Build dependencies
+RUN apt-get update; apt-get install nodejs npm -y; apt-get clean
+
+# Build options
+ARG BUILD_CONFIGURATION=Release
+
+# Download npm packages
+WORKDIR /src/Moonlight.Frontend/Styles
+
+COPY ["Moonlight.Frontend/Styles/package.json", "package.json"]
+
+RUN npm install
+
+# Restore nuget packages
+WORKDIR /src
+
+COPY ["Moonlight.Api/Moonlight.Api.csproj", "Moonlight.Api/"]
+COPY ["Moonlight.Frontend/Moonlight.Frontend.csproj", "Moonlight.Frontend/"]
+COPY ["Moonlight.Shared/Moonlight.Shared.csproj", "Moonlight.Shared/"]
+
+RUN dotnet restore "Moonlight.Api/Moonlight.Api.csproj"
+RUN dotnet restore "Moonlight.Frontend/Moonlight.Frontend.csproj"
+
+# Copy over the whole sources
+COPY . .
+
+# Build styles
+WORKDIR /src/Moonlight.Frontend/Styles
+RUN npm run tailwind-build
+
+# Build projects
+WORKDIR "/src/Moonlight.Api"
+RUN dotnet build "./Moonlight.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build-api
+
+WORKDIR "/src/Moonlight.Frontend"
+RUN dotnet build "./Moonlight.Frontend.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
+
+FROM build AS publish
+
+# Publish options
+ARG BUILD_CONFIGURATION=Release
+
+# Publish applications
+WORKDIR "/src/Moonlight.Api"
+RUN dotnet publish "./Moonlight.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish-api /p:UseAppHost=false
+
+WORKDIR "/src/Moonlight.Frontend"
+RUN dotnet publish "./Moonlight.Frontend.csproj" -c $BUILD_CONFIGURATION -o /app/publish-frontend /p:UseAppHost=false
+
+FROM base AS final
+
+WORKDIR /app
+
+COPY --from=publish /app/publish-api .
+COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot
+
+ENTRYPOINT ["dotnet", "Moonlight.Api.dll"]
\ No newline at end of file
diff --git a/Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj b/Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj
new file mode 100644
index 00000000..37e5935e
--- /dev/null
+++ b/Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+ .dockerignore
+
+
+
+
diff --git a/Hosts/Moonlight.Api.Host/Program.cs b/Hosts/Moonlight.Api.Host/Program.cs
new file mode 100644
index 00000000..c4b7d8f9
--- /dev/null
+++ b/Hosts/Moonlight.Api.Host/Program.cs
@@ -0,0 +1,22 @@
+using Moonlight.Api.Host;
+
+var appLoader = new AppStartupLoader();
+appLoader.Initialize();
+
+var builder = WebApplication.CreateBuilder(args);
+
+appLoader.PreBuild(builder);
+
+var app = builder.Build();
+
+appLoader.PostBuild(app);
+
+appLoader.PostMiddleware(app);
+
+if (app.Environment.IsDevelopment())
+ app.UseWebAssemblyDebugging();
+
+app.UseBlazorFrameworkFiles();
+app.UseStaticFiles();
+
+await app.RunAsync();
\ No newline at end of file
diff --git a/Hosts/Moonlight.Api.Host/Properties/launchSettings.json b/Hosts/Moonlight.Api.Host/Properties/launchSettings.json
new file mode 100644
index 00000000..cd3d7bcf
--- /dev/null
+++ b/Hosts/Moonlight.Api.Host/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5240",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Hosts/Moonlight.Frontend.Host/AppStartupLoader.cs b/Hosts/Moonlight.Frontend.Host/AppStartupLoader.cs
new file mode 100644
index 00000000..6ef20cd4
--- /dev/null
+++ b/Hosts/Moonlight.Frontend.Host/AppStartupLoader.cs
@@ -0,0 +1,10 @@
+using MoonCore.PluginFramework;
+using Moonlight.Frontend.Startup;
+
+namespace Moonlight.Frontend.Host;
+
+[PluginLoader]
+public partial class AppStartupLoader : IAppStartup
+{
+
+}
\ No newline at end of file
diff --git a/Moonlight.Client.Runtime/Moonlight.Client.Runtime.csproj b/Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj
similarity index 54%
rename from Moonlight.Client.Runtime/Moonlight.Client.Runtime.csproj
rename to Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj
index 4c81345a..2ecce0ad 100644
--- a/Moonlight.Client.Runtime/Moonlight.Client.Runtime.csproj
+++ b/Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj
@@ -1,28 +1,24 @@
- net9.0
+ net10.0
enable
enable
- True
+
+ True
+ true
+ true
+ true
-
-
-
-
+
+
-
-
-
-
+
-
-
-
diff --git a/Hosts/Moonlight.Frontend.Host/Program.cs b/Hosts/Moonlight.Frontend.Host/Program.cs
new file mode 100644
index 00000000..5b58db81
--- /dev/null
+++ b/Hosts/Moonlight.Frontend.Host/Program.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using Moonlight.Frontend.Host;
+
+var appLoader = new AppStartupLoader();
+appLoader.Initialize();
+
+var builder = WebAssemblyHostBuilder.CreateDefault(args);
+
+appLoader.PreBuild(builder);
+
+var app = builder.Build();
+
+appLoader.PostBuild(app);
+
+await app.RunAsync();
\ No newline at end of file
diff --git a/Moonlight.Client.Runtime/Properties/launchSettings.json b/Hosts/Moonlight.Frontend.Host/Properties/launchSettings.json
similarity index 68%
rename from Moonlight.Client.Runtime/Properties/launchSettings.json
rename to Hosts/Moonlight.Frontend.Host/Properties/launchSettings.json
index d9cbefe5..5c4e3431 100644
--- a/Moonlight.Client.Runtime/Properties/launchSettings.json
+++ b/Hosts/Moonlight.Frontend.Host/Properties/launchSettings.json
@@ -1,14 +1,15 @@
{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
- "launchBrowser": false,
+ "launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
- "applicationUrl": "http://localhost:5165",
+ "applicationUrl": "http://localhost:5250",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Hosts/Moonlight.Frontend.Host/Styles/package.json b/Hosts/Moonlight.Frontend.Host/Styles/package.json
new file mode 100644
index 00000000..f6df15b7
--- /dev/null
+++ b/Hosts/Moonlight.Frontend.Host/Styles/package.json
@@ -0,0 +1,15 @@
+{
+ "scripts": {
+ "tailwind": "npx postcss styles.css -o ../wwwroot/style.min.css --watch",
+ "tailwind-build": "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",
+ "shadcnblazor": "^1.0.4",
+ "tailwindcss": "^4.1.18",
+ "tw-animate-css": "^1.4.0"
+ }
+}
diff --git a/Hosts/Moonlight.Frontend.Host/Styles/postcss.config.mjs b/Hosts/Moonlight.Frontend.Host/Styles/postcss.config.mjs
new file mode 100644
index 00000000..8b9b8fba
--- /dev/null
+++ b/Hosts/Moonlight.Frontend.Host/Styles/postcss.config.mjs
@@ -0,0 +1,8 @@
+export default {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ "cssnano":{
+ preset: 'default'
+ }
+ }
+}
\ No newline at end of file
diff --git a/Hosts/Moonlight.Frontend.Host/Styles/styles.css b/Hosts/Moonlight.Frontend.Host/Styles/styles.css
new file mode 100644
index 00000000..19377e05
--- /dev/null
+++ b/Hosts/Moonlight.Frontend.Host/Styles/styles.css
@@ -0,0 +1,30 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@import "./node_modules/shadcnblazor/scrollbar.css";
+@import "./node_modules/shadcnblazor/default-theme.css";
+@import "./theme.css";
+@source "./node_modules/shadcnblazor/classes.json";
+
+@source "../../Moonlight.Api/**/*.razor";
+@source "../../Moonlight.Api/**/*.cs";
+@source "../../Moonlight.Api/**/*.html";
+
+@source "../../Moonlight.Frontend/**/*.razor";
+@source "../../Moonlight.Frontend/**/*.cs";
+@source "../../Moonlight.Frontend/**/*.html";
+
+@custom-variant dark (&:is(.dark *));
+
+#blazor-error-ui {
+ display: none;
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
\ No newline at end of file
diff --git a/Hosts/Moonlight.Frontend.Host/Styles/theme.css b/Hosts/Moonlight.Frontend.Host/Styles/theme.css
new file mode 100644
index 00000000..9e526acb
--- /dev/null
+++ b/Hosts/Moonlight.Frontend.Host/Styles/theme.css
@@ -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);
+}
\ No newline at end of file
diff --git a/Hosts/Moonlight.Frontend.Host/wwwroot/index.html b/Hosts/Moonlight.Frontend.Host/wwwroot/index.html
new file mode 100644
index 00000000..83af45b2
--- /dev/null
+++ b/Hosts/Moonlight.Frontend.Host/wwwroot/index.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+ Moonlight
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading application
+
+
+
+
+
+
+
+
+ An unhandled error has occurred.
+
Reload
+
🗙
+
+
+
+
+
+
+
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 0e259d42..00000000
--- a/LICENSE
+++ /dev/null
@@ -1,121 +0,0 @@
-Creative Commons Legal Code
-
-CC0 1.0 Universal
-
- CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
- LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
- ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
- INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
- REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
- PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
- THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
- HEREUNDER.
-
-Statement of Purpose
-
-The laws of most jurisdictions throughout the world automatically confer
-exclusive Copyright and Related Rights (defined below) upon the creator
-and subsequent owner(s) (each and all, an "owner") of an original work of
-authorship and/or a database (each, a "Work").
-
-Certain owners wish to permanently relinquish those rights to a Work for
-the purpose of contributing to a commons of creative, cultural and
-scientific works ("Commons") that the public can reliably and without fear
-of later claims of infringement build upon, modify, incorporate in other
-works, reuse and redistribute as freely as possible in any form whatsoever
-and for any purposes, including without limitation commercial purposes.
-These owners may contribute to the Commons to promote the ideal of a free
-culture and the further production of creative, cultural and scientific
-works, or to gain reputation or greater distribution for their Work in
-part through the use and efforts of others.
-
-For these and/or other purposes and motivations, and without any
-expectation of additional consideration or compensation, the person
-associating CC0 with a Work (the "Affirmer"), to the extent that he or she
-is an owner of Copyright and Related Rights in the Work, voluntarily
-elects to apply CC0 to the Work and publicly distribute the Work under its
-terms, with knowledge of his or her Copyright and Related Rights in the
-Work and the meaning and intended legal effect of CC0 on those rights.
-
-1. Copyright and Related Rights. A Work made available under CC0 may be
-protected by copyright and related or neighboring rights ("Copyright and
-Related Rights"). Copyright and Related Rights include, but are not
-limited to, the following:
-
- i. the right to reproduce, adapt, distribute, perform, display,
- communicate, and translate a Work;
- ii. moral rights retained by the original author(s) and/or performer(s);
-iii. publicity and privacy rights pertaining to a person's image or
- likeness depicted in a Work;
- iv. rights protecting against unfair competition in regards to a Work,
- subject to the limitations in paragraph 4(a), below;
- v. rights protecting the extraction, dissemination, use and reuse of data
- in a Work;
- vi. database rights (such as those arising under Directive 96/9/EC of the
- European Parliament and of the Council of 11 March 1996 on the legal
- protection of databases, and under any national implementation
- thereof, including any amended or successor version of such
- directive); and
-vii. other similar, equivalent or corresponding rights throughout the
- world based on applicable law or treaty, and any national
- implementations thereof.
-
-2. Waiver. To the greatest extent permitted by, but not in contravention
-of, applicable law, Affirmer hereby overtly, fully, permanently,
-irrevocably and unconditionally waives, abandons, and surrenders all of
-Affirmer's Copyright and Related Rights and associated claims and causes
-of action, whether now known or unknown (including existing as well as
-future claims and causes of action), in the Work (i) in all territories
-worldwide, (ii) for the maximum duration provided by applicable law or
-treaty (including future time extensions), (iii) in any current or future
-medium and for any number of copies, and (iv) for any purpose whatsoever,
-including without limitation commercial, advertising or promotional
-purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
-member of the public at large and to the detriment of Affirmer's heirs and
-successors, fully intending that such Waiver shall not be subject to
-revocation, rescission, cancellation, termination, or any other legal or
-equitable action to disrupt the quiet enjoyment of the Work by the public
-as contemplated by Affirmer's express Statement of Purpose.
-
-3. Public License Fallback. Should any part of the Waiver for any reason
-be judged legally invalid or ineffective under applicable law, then the
-Waiver shall be preserved to the maximum extent permitted taking into
-account Affirmer's express Statement of Purpose. In addition, to the
-extent the Waiver is so judged Affirmer hereby grants to each affected
-person a royalty-free, non transferable, non sublicensable, non exclusive,
-irrevocable and unconditional license to exercise Affirmer's Copyright and
-Related Rights in the Work (i) in all territories worldwide, (ii) for the
-maximum duration provided by applicable law or treaty (including future
-time extensions), (iii) in any current or future medium and for any number
-of copies, and (iv) for any purpose whatsoever, including without
-limitation commercial, advertising or promotional purposes (the
-"License"). The License shall be deemed effective as of the date CC0 was
-applied by Affirmer to the Work. Should any part of the License for any
-reason be judged legally invalid or ineffective under applicable law, such
-partial invalidity or ineffectiveness shall not invalidate the remainder
-of the License, and in such case Affirmer hereby affirms that he or she
-will not (i) exercise any of his or her remaining Copyright and Related
-Rights in the Work or (ii) assert any associated claims and causes of
-action with respect to the Work, in either case contrary to Affirmer's
-express Statement of Purpose.
-
-4. Limitations and Disclaimers.
-
- a. No trademark or patent rights held by Affirmer are waived, abandoned,
- surrendered, licensed or otherwise affected by this document.
- b. Affirmer offers the Work as-is and makes no representations or
- warranties of any kind concerning the Work, express, implied,
- statutory or otherwise, including without limitation warranties of
- title, merchantability, fitness for a particular purpose, non
- infringement, or the absence of latent or other defects, accuracy, or
- the present or absence of errors, whether or not discoverable, all to
- the greatest extent permissible under applicable law.
- c. Affirmer disclaims responsibility for clearing rights of other persons
- that may apply to the Work or any use thereof, including without
- limitation any person's Copyright and Related Rights in the Work.
- Further, Affirmer disclaims responsibility for obtaining any necessary
- consents, permissions or other rights required for any use of the
- Work.
- d. Affirmer understands and acknowledges that Creative Commons is not a
- party to this document and has no duty or obligation with respect to
- this CC0 or use of the Work.
diff --git a/Moonlight.Api/Configuration/DatabaseOptions.cs b/Moonlight.Api/Configuration/DatabaseOptions.cs
new file mode 100644
index 00000000..4685d628
--- /dev/null
+++ b/Moonlight.Api/Configuration/DatabaseOptions.cs
@@ -0,0 +1,10 @@
+namespace Moonlight.Api.Configuration;
+
+public class DatabaseOptions
+{
+ public string Host { get; set; }
+ public int Port { get; set; } = 5432;
+ public string Username { get; set; }
+ public string Password { get; set; }
+ public string Database { get; set; }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Configuration/OidcOptions.cs b/Moonlight.Api/Configuration/OidcOptions.cs
new file mode 100644
index 00000000..a99cda3d
--- /dev/null
+++ b/Moonlight.Api/Configuration/OidcOptions.cs
@@ -0,0 +1,11 @@
+namespace Moonlight.Api.Configuration;
+
+public class OidcOptions
+{
+ public string Authority { get; set; }
+ public bool RequireHttpsMetadata { get; set; } = true;
+ public string ResponseType { get; set; } = "code";
+ public string[]? Scopes { get; set; }
+ public string ClientId { get; set; }
+ public string ClientSecret { get; set; }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Database/DataContext.cs b/Moonlight.Api/Database/DataContext.cs
new file mode 100644
index 00000000..ddddf1cc
--- /dev/null
+++ b/Moonlight.Api/Database/DataContext.cs
@@ -0,0 +1,32 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Options;
+using Moonlight.Api.Configuration;
+using Moonlight.Api.Database.Entities;
+
+namespace Moonlight.Api.Database;
+
+public class DataContext : DbContext
+{
+ public DbSet Users { get; set; }
+
+ private readonly IOptions Options;
+
+ public DataContext(IOptions 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}"
+ );
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Database/DatabaseRepository.cs b/Moonlight.Api/Database/DatabaseRepository.cs
new file mode 100644
index 00000000..ea153853
--- /dev/null
+++ b/Moonlight.Api/Database/DatabaseRepository.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace Moonlight.Api.Database;
+
+public class DatabaseRepository where T : class
+{
+ private readonly DataContext DataContext;
+ private readonly DbSet Set;
+
+ public DatabaseRepository(DataContext dataContext)
+ {
+ DataContext = dataContext;
+ Set = DataContext.Set();
+ }
+
+ public IQueryable Query() => Set;
+
+ public async Task AddAsync(T entity)
+ {
+ var final = Set.Add(entity);
+ await DataContext.SaveChangesAsync();
+ return final.Entity;
+ }
+
+ public async Task UpdateAsync(T entity)
+ {
+ Set.Update(entity);
+ await DataContext.SaveChangesAsync();
+ }
+
+ public async Task RemoveAsync(T entity)
+ {
+ Set.Remove(entity);
+ await DataContext.SaveChangesAsync();
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Database/Entities/User.cs b/Moonlight.Api/Database/Entities/User.cs
new file mode 100644
index 00000000..f5fcf8b4
--- /dev/null
+++ b/Moonlight.Api/Database/Entities/User.cs
@@ -0,0 +1,11 @@
+namespace Moonlight.Api.Database.Entities;
+
+public class User
+{
+ public int Id { get; set; }
+
+ public string Username { get; set; }
+ public string Email { get; set; }
+
+ public DateTimeOffset InvalidateTimestamp { get; set; }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Database/Migrations/20251216083232_AddedUsers.Designer.cs b/Moonlight.Api/Database/Migrations/20251216083232_AddedUsers.Designer.cs
new file mode 100644
index 00000000..8b45c33a
--- /dev/null
+++ b/Moonlight.Api/Database/Migrations/20251216083232_AddedUsers.Designer.cs
@@ -0,0 +1,55 @@
+//
+
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Moonlight.Api.Database;
+
+#nullable disable
+
+namespace Moonlight.Api.Database.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20251216083232_AddedUsers")]
+ partial class AddedUsers
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("InvalidateTimestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Database/Migrations/20251216083232_AddedUsers.cs b/Moonlight.Api/Database/Migrations/20251216083232_AddedUsers.cs
new file mode 100644
index 00000000..9ecca052
--- /dev/null
+++ b/Moonlight.Api/Database/Migrations/20251216083232_AddedUsers.cs
@@ -0,0 +1,37 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Moonlight.Api.Database.Migrations
+{
+ ///
+ public partial class AddedUsers : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy",
+ NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Username = table.Column(type: "text", nullable: false),
+ Email = table.Column(type: "text", nullable: false),
+ InvalidateTimestamp =
+ table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table => { table.PrimaryKey("PK_Users", x => x.Id); });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Users");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight.Api/Database/Migrations/DataContextModelSnapshot.cs
new file mode 100644
index 00000000..fabcebfb
--- /dev/null
+++ b/Moonlight.Api/Database/Migrations/DataContextModelSnapshot.cs
@@ -0,0 +1,52 @@
+//
+
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Moonlight.Api.Database;
+
+#nullable disable
+
+namespace Moonlight.Api.Database.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ partial class DataContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("InvalidateTimestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Helpers/AppConsoleFormatter.cs b/Moonlight.Api/Helpers/AppConsoleFormatter.cs
new file mode 100644
index 00000000..d5b7136a
--- /dev/null
+++ b/Moonlight.Api/Helpers/AppConsoleFormatter.cs
@@ -0,0 +1,77 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Console;
+
+namespace Moonlight.Api.Helpers;
+
+public class AppConsoleFormatter : ConsoleFormatter
+{
+ private const string TimestampColor = "\e[38;2;148;148;148m";
+ private const string CategoryColor = "\e[38;2;198;198;198m";
+ private const string MessageColor = "\e[38;2;255;255;255m";
+ private const string Bold = "\e[1m";
+
+ // Pre-computed ANSI color codes for each log level
+ private const string CriticalColor = "\e[38;2;255;0;0m";
+ private const string ErrorColor = "\e[38;2;255;0;0m";
+ private const string WarningColor = "\e[38;2;215;215;0m";
+ private const string InfoColor = "\e[38;2;135;215;255m";
+ private const string DebugColor = "\e[38;2;198;198;198m";
+ private const string TraceColor = "\e[38;2;68;68;68m";
+
+ public AppConsoleFormatter() : base(nameof(AppConsoleFormatter))
+ {
+ }
+
+ public override void Write(
+ in LogEntry logEntry,
+ IExternalScopeProvider? scopeProvider,
+ TextWriter textWriter)
+ {
+ var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
+
+ // Timestamp
+ textWriter.Write(TimestampColor);
+ textWriter.Write(DateTime.Now.ToString("dd.MM.yy HH:mm:ss"));
+ textWriter.Write(' ');
+
+ // Log level with color and bold
+ var (levelText, levelColor) = GetLevelInfo(logEntry.LogLevel);
+ textWriter.Write(levelColor);
+ textWriter.Write(Bold);
+ textWriter.Write(levelText);
+ textWriter.Write(' ');
+
+ // Category
+ textWriter.Write(CategoryColor);
+ textWriter.Write(logEntry.Category);
+
+ // Message
+ textWriter.Write(MessageColor);
+ textWriter.Write(": ");
+ textWriter.Write(message);
+
+ // Exception
+ if (logEntry.Exception != null)
+ {
+ textWriter.Write(MessageColor);
+ textWriter.WriteLine(logEntry.Exception.ToString());
+ }
+ else
+ textWriter.WriteLine();
+ }
+
+ private static (string text, string color) GetLevelInfo(LogLevel logLevel)
+ {
+ return logLevel switch
+ {
+ LogLevel.Critical => ("CRIT", CriticalColor),
+ LogLevel.Error => ("ERRO", ErrorColor),
+ LogLevel.Warning => ("WARN", WarningColor),
+ LogLevel.Information => ("INFO", InfoColor),
+ LogLevel.Debug => ("DEBG", DebugColor),
+ LogLevel.Trace => ("TRCE", TraceColor),
+ _ => ("NONE", "")
+ };
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Http/Controllers/AuthController.cs b/Moonlight.Api/Http/Controllers/AuthController.cs
new file mode 100644
index 00000000..c7401736
--- /dev/null
+++ b/Moonlight.Api/Http/Controllers/AuthController.cs
@@ -0,0 +1,65 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Moonlight.Shared.Http.Responses.Auth;
+
+namespace Moonlight.Api.Http.Controllers;
+
+[ApiController]
+[Route("api/auth")]
+public class AuthController : Controller
+{
+ private readonly IAuthenticationSchemeProvider SchemeProvider;
+
+ public AuthController(IAuthenticationSchemeProvider schemeProvider)
+ {
+ SchemeProvider = schemeProvider;
+ }
+
+ [HttpGet]
+ public async Task> GetSchemesAsync()
+ {
+ var schemes = await SchemeProvider.GetAllSchemesAsync();
+
+ return schemes
+ .Where(scheme => !string.IsNullOrWhiteSpace(scheme.DisplayName))
+ .Select(scheme => new SchemeResponse(scheme.Name, scheme.DisplayName!))
+ .ToArray();
+ }
+
+ [HttpGet("{schemeName:alpha}")]
+ public async Task StartAsync([FromRoute] string schemeName)
+ {
+ var scheme = await SchemeProvider.GetSchemeAsync(schemeName);
+
+ if (scheme == null || string.IsNullOrWhiteSpace(scheme.DisplayName))
+ return Problem("Invalid authentication scheme name", statusCode: 400);
+
+ return Challenge(new AuthenticationProperties()
+ {
+ RedirectUri = "/"
+ }, scheme.Name);
+ }
+
+ [Authorize]
+ [HttpGet("claims")]
+ public Task> GetClaimsAsync()
+ {
+ var result = User.Claims
+ .Select(claim => new ClaimResponse(claim.Type, claim.Value))
+ .ToArray();
+
+ return Task.FromResult>(result);
+ }
+
+ [HttpGet("logout")]
+ public Task LogoutAsync()
+ {
+ return Task.FromResult(
+ SignOut(new AuthenticationProperties()
+ {
+ RedirectUri = "/"
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Http/Controllers/UsersController.cs b/Moonlight.Api/Http/Controllers/UsersController.cs
new file mode 100644
index 00000000..6356ea69
--- /dev/null
+++ b/Moonlight.Api/Http/Controllers/UsersController.cs
@@ -0,0 +1,126 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Moonlight.Shared.Http.Requests;
+using Moonlight.Shared.Http.Requests.Users;
+using Moonlight.Shared.Http.Responses;
+using Moonlight.Shared.Http.Responses.Users;
+using Moonlight.Api.Database;
+using Moonlight.Api.Database.Entities;
+using Moonlight.Api.Mappers;
+
+namespace Moonlight.Api.Http.Controllers;
+
+[Authorize]
+[ApiController]
+[Route("api/users")]
+public class UsersController : Controller
+{
+ private readonly DatabaseRepository UserRepository;
+
+ public UsersController(DatabaseRepository userRepository)
+ {
+ UserRepository = userRepository;
+ }
+
+ [HttpGet]
+ public async Task>> 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");
+
+ var query = UserRepository
+ .Query();
+
+ // Filters
+ if (filterOptions != null)
+ {
+ foreach (var filterOption in filterOptions.Filters)
+ {
+ query = filterOption.Key switch
+ {
+ nameof(Database.Entities.User.Email) =>
+ query.Where(user => EF.Functions.ILike(user.Email, $"%{filterOption.Value}%")),
+
+ nameof(Database.Entities.User.Username) =>
+ query.Where(user => EF.Functions.ILike(user.Username, $"%{filterOption.Value}%")),
+
+ _ => query
+ };
+ }
+ }
+
+ // Pagination
+ var data = await query
+ .ProjectToResponse()
+ .Skip(startIndex)
+ .Take(length)
+ .ToArrayAsync();
+
+ var total = await query.CountAsync();
+
+ return new PagedData(data, total);
+ }
+
+ [HttpGet("{id:int}")]
+ public async Task> GetAsync([FromRoute] int id)
+ {
+ var user = await UserRepository
+ .Query()
+ .FirstOrDefaultAsync(x => x.Id == id);
+
+ if (user == null)
+ return Problem("No user with this id found", statusCode: 404);
+
+ return UserMapper.MapToResponse(user);
+ }
+
+ [HttpPost]
+ public async Task> CreateAsync([FromBody] CreateUserRequest request)
+ {
+ var user = UserMapper.MapToUser(request);
+ user.InvalidateTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1);
+
+ var finalUser = await UserRepository.AddAsync(user);
+
+ return UserMapper.MapToResponse(finalUser);
+ }
+
+ [HttpPatch("{id:int}")]
+ public async Task> UpdateAsync([FromRoute] int id, [FromBody] UpdateUserRequest request)
+ {
+ var user = await UserRepository
+ .Query()
+ .FirstOrDefaultAsync(x => x.Id == id);
+
+ if (user == null)
+ return Problem("No user with this id found", statusCode: 404);
+
+ UserMapper.Merge(user, request);
+ await UserRepository.UpdateAsync(user);
+
+ return UserMapper.MapToResponse(user);
+ }
+
+ [HttpDelete("{id:int}")]
+ public async Task DeleteAsync([FromRoute] int id)
+ {
+ var user = await UserRepository
+ .Query()
+ .FirstOrDefaultAsync(user => user.Id == id);
+
+ if (user == null)
+ return Problem("No user with this id found", statusCode: 404);
+
+ await UserRepository.RemoveAsync(user);
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Mappers/UserMapper.cs b/Moonlight.Api/Mappers/UserMapper.cs
new file mode 100644
index 00000000..a70b8fc4
--- /dev/null
+++ b/Moonlight.Api/Mappers/UserMapper.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics.CodeAnalysis;
+using Riok.Mapperly.Abstractions;
+using Moonlight.Shared.Http.Requests.Users;
+using Moonlight.Shared.Http.Responses.Users;
+using Moonlight.Api.Database.Entities;
+
+namespace Moonlight.Api.Mappers;
+
+[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 UserMapper
+{
+ public static partial IQueryable ProjectToResponse(this IQueryable users);
+
+ public static partial UserResponse MapToResponse(User user);
+
+ public static partial void Merge([MappingTarget] User user, UpdateUserRequest request);
+
+ public static partial User MapToUser(CreateUserRequest request);
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Moonlight.Api.csproj b/Moonlight.Api/Moonlight.Api.csproj
new file mode 100644
index 00000000..b257bf03
--- /dev/null
+++ b/Moonlight.Api/Moonlight.Api.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net10.0
+ enable
+ enable
+ Linux
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
\ No newline at end of file
diff --git a/Moonlight.Api/Properties/launchSettings.json b/Moonlight.Api/Properties/launchSettings.json
new file mode 100644
index 00000000..97f83af3
--- /dev/null
+++ b/Moonlight.Api/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5185",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Moonlight.Api/Services/DbMigrationService.cs b/Moonlight.Api/Services/DbMigrationService.cs
new file mode 100644
index 00000000..4d300a47
--- /dev/null
+++ b/Moonlight.Api/Services/DbMigrationService.cs
@@ -0,0 +1,49 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Moonlight.Api.Database;
+
+namespace Moonlight.Api.Services;
+
+public class DbMigrationService : IHostedLifecycleService
+{
+ private readonly ILogger Logger;
+ private readonly IServiceProvider ServiceProvider;
+
+ public DbMigrationService(ILogger 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();
+
+ var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken);
+ var migrationNames = pendingMigrations.ToArray();
+
+ if (migrationNames.Length == 0)
+ {
+ Logger.LogDebug("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;
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Services/UserAuthService.cs b/Moonlight.Api/Services/UserAuthService.cs
new file mode 100644
index 00000000..2c6bb267
--- /dev/null
+++ b/Moonlight.Api/Services/UserAuthService.cs
@@ -0,0 +1,99 @@
+using System.Security.Claims;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Moonlight.Api.Database;
+using Moonlight.Api.Database.Entities;
+
+namespace Moonlight.Api.Services;
+
+public class UserAuthService
+{
+ private readonly DatabaseRepository UserRepository;
+ private readonly ILogger Logger;
+
+ private const string UserIdClaim = "UserId";
+ private const string IssuedAtClaim = "IssuedAt";
+
+ public UserAuthService(DatabaseRepository userRepository, ILogger logger)
+ {
+ UserRepository = userRepository;
+ Logger = logger;
+ }
+
+ public async Task SyncAsync(ClaimsPrincipal? principal)
+ {
+ if (principal is null)
+ return false;
+
+ var username = principal.FindFirstValue(ClaimTypes.Name);
+ var email = principal.FindFirstValue(ClaimTypes.Email);
+
+ if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(email))
+ {
+ Logger.LogWarning("Unable to sync user to database as name and/or email claims are missing");
+ return false;
+ }
+
+ // We use email as the primary identifier here
+ var user = await UserRepository
+ .Query()
+ .AsNoTracking()
+ .FirstOrDefaultAsync(user => user.Email == email);
+
+ if (user == null) // Sync user if not already existing in the database
+ {
+ user = await UserRepository.AddAsync(new User()
+ {
+ Username = username,
+ Email = email,
+ InvalidateTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1)
+ });
+ }
+ else // Update properties of existing user
+ {
+ user.Username = username;
+
+ await UserRepository.UpdateAsync(user);
+ }
+
+ principal.Identities.First().AddClaims([
+ new Claim(UserIdClaim, user.Id.ToString()),
+ new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
+ ]);
+
+ return true;
+ }
+
+ public async Task ValidateAsync(ClaimsPrincipal? principal)
+ {
+ // Ignore malformed claims principal
+ if (principal is not { Identity.IsAuthenticated: true })
+ return false;
+
+ var userIdString = principal.FindFirstValue(UserIdClaim);
+
+ if (!int.TryParse(userIdString, out var userId))
+ return false;
+
+ var user = await UserRepository
+ .Query()
+ .AsNoTracking()
+ .FirstOrDefaultAsync(user => user.Id == userId);
+
+ if (user == null)
+ return false;
+
+ var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
+
+ if (!long.TryParse(issuedAtString, out var issuedAtUnix))
+ return false;
+
+ var issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedAtUnix).ToUniversalTime();
+
+ // If the issued at timestamp is greater than the token validation timestamp
+ // everything is fine. If not it means that the token should be invalidated
+ // as it is too old
+
+ return issuedAt > user.InvalidateTimestamp;
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Startup/IAppStartup.cs b/Moonlight.Api/Startup/IAppStartup.cs
new file mode 100644
index 00000000..7f49b7d7
--- /dev/null
+++ b/Moonlight.Api/Startup/IAppStartup.cs
@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Builder;
+
+namespace Moonlight.Api.Startup;
+
+public interface IAppStartup
+{
+ public void PreBuild(WebApplicationBuilder builder);
+ public void PostBuild(WebApplication application);
+ public void PostMiddleware(WebApplication application);
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Startup/Startup.Auth.cs b/Moonlight.Api/Startup/Startup.Auth.cs
new file mode 100644
index 00000000..8f48f3f8
--- /dev/null
+++ b/Moonlight.Api/Startup/Startup.Auth.cs
@@ -0,0 +1,91 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Moonlight.Api.Configuration;
+using Moonlight.Api.Services;
+
+namespace Moonlight.Api.Startup;
+
+public partial class Startup
+{
+ private static void AddAuth(WebApplicationBuilder builder)
+ {
+ var oidcOptions = new OidcOptions();
+ builder.Configuration.GetSection("WebApp:Oidc").Bind(oidcOptions);
+
+ builder.Services.AddScoped();
+
+ builder.Services.AddAuthentication("Session")
+ .AddCookie("Session", null, options =>
+ {
+ options.Events.OnSigningIn += async context =>
+ {
+ var authService = context
+ .HttpContext
+ .RequestServices
+ .GetRequiredService();
+
+ var result = await authService.SyncAsync(context.Principal);
+
+ if (result)
+ context.Properties.IsPersistent = true;
+ else
+ context.Principal = new ClaimsPrincipal();
+ };
+
+ options.Events.OnValidatePrincipal += async context =>
+ {
+ var authService = context
+ .HttpContext
+ .RequestServices
+ .GetRequiredService();
+
+ var result = await authService.ValidateAsync(context.Principal);
+
+ if (!result)
+ context.RejectPrincipal();
+ };
+
+ options.Cookie = new CookieBuilder()
+ {
+ Name = "token",
+ Path = "/",
+ IsEssential = true,
+ SecurePolicy = CookieSecurePolicy.SameAsRequest
+ };
+ })
+ .AddOpenIdConnect("OIDC", "OpenID Connect", options =>
+ {
+ options.Authority = oidcOptions.Authority;
+ options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata;
+
+ var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
+
+ options.Scope.Clear();
+
+ foreach (var scope in scopes)
+ options.Scope.Add(scope);
+
+ options.ResponseType = oidcOptions.ResponseType;
+ options.ClientId = oidcOptions.ClientId;
+ options.ClientSecret = oidcOptions.ClientSecret;
+
+ options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
+ options.ClaimActions.MapJsonKey(ClaimTypes.Name, "preferred_username");
+ options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
+
+ options.GetClaimsFromUserInfoEndpoint = true;
+ });
+
+ builder.Services.AddAuthorization();
+ }
+
+ private static void UseAuth(WebApplication application)
+ {
+ application.UseAuthentication();
+ application.UseAuthorization();
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Startup/Startup.Base.cs b/Moonlight.Api/Startup/Startup.Base.cs
new file mode 100644
index 00000000..1acba450
--- /dev/null
+++ b/Moonlight.Api/Startup/Startup.Base.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Console;
+using Moonlight.Shared.Http;
+using Moonlight.Api.Helpers;
+
+namespace Moonlight.Api.Startup;
+
+public partial class Startup
+{
+ private static void AddBase(WebApplicationBuilder builder)
+ {
+ builder.Services.AddControllers().AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
+ });
+
+ builder.Logging.ClearProviders();
+ builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
+ builder.Logging.AddConsoleFormatter();
+ }
+
+ private static void UseBase(WebApplication application)
+ {
+
+ application.UseRouting();
+ }
+
+ private static void MapBase(WebApplication application)
+ {
+ application.MapControllers();
+
+ application.MapFallbackToFile("index.html");
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Startup/Startup.Database.cs b/Moonlight.Api/Startup/Startup.Database.cs
new file mode 100644
index 00000000..d60678e6
--- /dev/null
+++ b/Moonlight.Api/Startup/Startup.Database.cs
@@ -0,0 +1,19 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Moonlight.Api.Configuration;
+using Moonlight.Api.Database;
+using Moonlight.Api.Services;
+
+namespace Moonlight.Api.Startup;
+
+public partial class Startup
+{
+ private static void AddDatabase(WebApplicationBuilder builder)
+ {
+ builder.Services.AddOptions().BindConfiguration("WebApp:Database");
+
+ builder.Services.AddDbContext();
+ builder.Services.AddScoped(typeof(DatabaseRepository<>));
+ builder.Services.AddHostedService();
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Api/Startup/Startup.cs b/Moonlight.Api/Startup/Startup.cs
new file mode 100644
index 00000000..e5066df1
--- /dev/null
+++ b/Moonlight.Api/Startup/Startup.cs
@@ -0,0 +1,24 @@
+using Microsoft.AspNetCore.Builder;
+
+namespace Moonlight.Api.Startup;
+
+public partial class Startup : IAppStartup
+{
+ public void PreBuild(WebApplicationBuilder builder)
+ {
+ AddBase(builder);
+ AddAuth(builder);
+ AddDatabase(builder);
+ }
+
+ public void PostBuild(WebApplication application)
+ {
+ UseBase(application);
+ UseAuth(application);
+ }
+
+ public void PostMiddleware(WebApplication application)
+ {
+ MapBase(application);
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.ApiServer.Runtime/Moonlight.ApiServer.Runtime.csproj b/Moonlight.ApiServer.Runtime/Moonlight.ApiServer.Runtime.csproj
deleted file mode 100644
index 44073a4e..00000000
--- a/Moonlight.ApiServer.Runtime/Moonlight.ApiServer.Runtime.csproj
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- net9.0
- enable
- enable
- True
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
diff --git a/Moonlight.ApiServer.Runtime/PluginLoader.cs b/Moonlight.ApiServer.Runtime/PluginLoader.cs
deleted file mode 100644
index 2d5fc003..00000000
--- a/Moonlight.ApiServer.Runtime/PluginLoader.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using MoonCore.PluginFramework;
-using Moonlight.ApiServer.Plugins;
-
-namespace Moonlight.ApiServer.Runtime;
-
-[PluginLoader]
-public partial class PluginLoader : IPluginStartup
-{
-
-}
\ No newline at end of file
diff --git a/Moonlight.ApiServer.Runtime/Plugins.props b/Moonlight.ApiServer.Runtime/Plugins.props
deleted file mode 100644
index 3e0a6663..00000000
--- a/Moonlight.ApiServer.Runtime/Plugins.props
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/Moonlight.ApiServer.Runtime/Program.cs b/Moonlight.ApiServer.Runtime/Program.cs
deleted file mode 100644
index 5bb192b2..00000000
--- a/Moonlight.ApiServer.Runtime/Program.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Moonlight.ApiServer.Configuration;
-using Moonlight.ApiServer.Runtime;
-using Moonlight.ApiServer.Startup;
-
-var pluginLoader = new PluginLoader();
-pluginLoader.Initialize();
-
-var builder = WebApplication.CreateBuilder(args);
-
-builder.AddMoonlight(pluginLoader.Instances);
-
-var app = builder.Build();
-
-app.UseMoonlight(pluginLoader.Instances);
-
-// Add frontend
-var configuration = AppConfiguration.CreateEmpty();
-builder.Configuration.Bind(configuration);
-
-// Handle setup of wasm app hosting in the runtime
-// so the Moonlight.ApiServer doesn't need the wasm package
-if (configuration.Frontend.EnableHosting)
-{
- if (app.Environment.IsDevelopment())
- app.UseWebAssemblyDebugging();
-
- app.UseBlazorFrameworkFiles();
- app.UseStaticFiles();
-}
-
-app.MapMoonlight(pluginLoader.Instances);
-
-
-await app.RunAsync();
\ No newline at end of file
diff --git a/Moonlight.ApiServer.Runtime/Properties/launchSettings.json b/Moonlight.ApiServer.Runtime/Properties/launchSettings.json
deleted file mode 100644
index 8c3160eb..00000000
--- a/Moonlight.ApiServer.Runtime/Properties/launchSettings.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "profiles": {
- "http": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": false,
- "applicationUrl": "http://localhost:5165",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "HTTP_PROXY": "",
- "HTTPS_PROXY": ""
- },
- "hotReloadEnabled": true
- },
- "WASM Debug": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": false,
- "applicationUrl": "http://localhost:5165",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "HTTP_PROXY": "",
- "HTTPS_PROXY": ""
- },
- "hotReloadEnabled": true,
- "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
- }
- }
-}
\ No newline at end of file
diff --git a/Moonlight.ApiServer/Configuration/AppConfiguration.cs b/Moonlight.ApiServer/Configuration/AppConfiguration.cs
deleted file mode 100644
index bdcc9b42..00000000
--- a/Moonlight.ApiServer/Configuration/AppConfiguration.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using MoonCore.Helpers;
-using Moonlight.ApiServer.Implementations.LocalAuth;
-using YamlDotNet.Serialization;
-
-namespace Moonlight.ApiServer.Configuration;
-
-public record AppConfiguration
-{
- [YamlMember(Description = "Moonlight configuration\n\n\nThe public url your instance should be accessible through")]
- public string PublicUrl { get; set; } = "http://localhost:5165";
-
- [YamlMember(Description = "\nThe credentials of the postgres which moonlight should use")]
- public DatabaseConfig Database { get; set; } = new();
-
- [YamlMember(Description = "\nSettings regarding authentication")]
- public AuthenticationConfig Authentication { get; set; } = new();
-
- [YamlMember(Description = "\nThese options are only meant for development purposes")]
- public DevelopmentConfig Development { get; set; } = new();
-
- [YamlMember(Description = "\nSettings for hosting the frontend")]
- public FrontendData Frontend { get; set; } = new();
-
- [YamlMember(Description = "\nSettings for the internal web server moonlight is running in")]
- public KestrelConfig Kestrel { get; set; } = new();
-
- [YamlMember(Description = "\nSettings for the internal file manager for moonlights storage access")]
- public FilesData Files { get; set; } = new();
-
- [YamlMember(Description = "\nSettings for open telemetry")]
- public OpenTelemetryData OpenTelemetry { get; set; } = new();
-
- [YamlMember(Description = "\nConfiguration for the realtime communication solution SignalR")]
- public SignalRData SignalR { get; set; } = new();
-
- public static AppConfiguration CreateEmpty()
- {
- return new AppConfiguration()
- {
- // Set arrays as empty here
-
- Kestrel = new()
- {
- AllowedOrigins = []
- },
- Authentication = new()
- {
- EnabledSchemes = []
- }
- };
- }
-
- public record SignalRData
- {
- [YamlMember(Description =
- "\nWhether to use redis (or any other redis compatible solution) to scale out SignalR hubs. This is required when using multiple api server replicas")]
- public bool UseRedis { get; set; } = false;
-
- public string RedisConnectionString { get; set; } = "";
- }
-
- public record FilesData
- {
- [YamlMember(Description = "The maximum file size limit a combine operation is allowed to process")]
- public double CombineLimit { get; set; } = ByteConverter.FromGigaBytes(5).MegaBytes;
- }
-
- public record FrontendData
- {
- [YamlMember(Description = "Enable the hosting of the frontend. Disable this if you only want to run the api server")]
- public bool EnableHosting { get; set; } = true;
- }
-
- public record DatabaseConfig
- {
- public string Host { get; set; } = "your-database-host.name";
- public int Port { get; set; } = 5432;
-
- public string Username { get; set; } = "db_user";
- public string Password { get; set; } = "db_password";
-
- public string Database { get; set; } = "db_name";
- }
-
- public record AuthenticationConfig
- {
- [YamlMember(Description = "The secret token to use for creating jwts and encrypting things. This needs to be at least 32 characters long")]
- public string Secret { get; set; } = Formatter.GenerateString(32);
-
- [YamlMember(Description = "Settings for the user sessions")]
- public SessionsConfig Sessions { get; set; } = new();
-
- [YamlMember(Description = "This specifies if the first registered/synced user will become an admin automatically")]
- public bool FirstUserAdmin { get; set; } = true;
-
- [YamlMember(Description = "This specifies the authentication schemes the frontend should be able to challenge")]
- public string[] EnabledSchemes { get; set; } = [LocalAuthConstants.AuthenticationScheme];
- }
-
- public record SessionsConfig
- {
- public string CookieName { get; set; } = "session";
- public int ExpiresIn { get; set; } = 10;
- }
-
- public record DevelopmentConfig
- {
- [YamlMember(Description = "This toggles the availability of the api docs via /api/swagger")]
- public bool EnableApiDocs { get; set; } = false;
- }
-
- public record KestrelConfig
- {
- [YamlMember(Description = "The upload limit in megabytes for the api server")]
- public int UploadLimit { get; set; } = 100;
-
- [YamlMember(Description = "The allowed origins for the api server. Use * to allow all origins (which is not advised)")]
- public string[] AllowedOrigins { get; set; } = ["*"];
- }
-
- public record OpenTelemetryData
- {
- [YamlMember(Description = "This enables open telemetry for moonlight")]
- public bool Enable { get; set; } = false;
-
- public OpenTelemetryMetricsData Metrics { get; set; } = new();
- public OpenTelemetryTracesData Traces { get; set; } = new();
- public OpenTelemetryLogsData Logs { get; set; } = new();
- }
-
- public record OpenTelemetryMetricsData
- {
- [YamlMember(Description = "This enables the exporting of metrics")]
- public bool Enable { get; set; } = true;
-
- [YamlMember(Description = "Enables the /metrics exporter for prometheus")]
- public bool EnablePrometheus { get; set; } = false;
-
- [YamlMember(Description = "The interval in which metrics are created, specified in seconds")]
- public int Interval { get; set; } = 15;
- }
-
- public record OpenTelemetryTracesData
- {
- [YamlMember(Description = "This enables the exporting of traces")]
- public bool Enable { get; set; } = true;
- }
-
- public record OpenTelemetryLogsData
- {
- [YamlMember(Description = "This enables the exporting of logs")]
- public bool Enable { get; set; } = true;
- }
-}
\ No newline at end of file
diff --git a/Moonlight.ApiServer/Database/CoreDataContext.cs b/Moonlight.ApiServer/Database/CoreDataContext.cs
deleted file mode 100644
index 5f6df282..00000000
--- a/Moonlight.ApiServer/Database/CoreDataContext.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using Hangfire.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore;
-using Moonlight.ApiServer.Configuration;
-using Moonlight.ApiServer.Database.Entities;
-using Moonlight.ApiServer.Models;
-
-namespace Moonlight.ApiServer.Database;
-
-public class CoreDataContext : DbContext
-{
- private readonly AppConfiguration Configuration;
-
- public DbSet Users { get; set; }
- public DbSet ApiKeys { get; set; }
- public DbSet Themes { get; set; }
-
- public CoreDataContext(AppConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- if(optionsBuilder.IsConfigured)
- return;
-
- var database = Configuration.Database;
-
- var connectionString = $"Host={database.Host};" +
- $"Port={database.Port};" +
- $"Database={database.Database};" +
- $"Username={database.Username};" +
- $"Password={database.Password}";
-
- optionsBuilder.UseNpgsql(connectionString, builder =>
- {
- builder.MigrationsHistoryTable("MigrationsHistory", "core");
- });
- }
-
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Model.SetDefaultSchema("core");
-
- base.OnModelCreating(modelBuilder);
- modelBuilder.OnHangfireModelCreating();
-
- modelBuilder.Ignore();
- modelBuilder.Entity()
- .OwnsOne(x => x.Content, builder =>
- {
- builder.ToJson();
- });
- }
-}
\ No newline at end of file
diff --git a/Moonlight.ApiServer/Database/Entities/ApiKey.cs b/Moonlight.ApiServer/Database/Entities/ApiKey.cs
deleted file mode 100644
index 9b75d2ec..00000000
--- a/Moonlight.ApiServer/Database/Entities/ApiKey.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Moonlight.ApiServer.Database.Entities;
-
-public class ApiKey
-{
- public int Id { get; set; }
-
- public string Description { get; set; }
-
- public string[] Permissions { get; set; } = [];
-
- public DateTimeOffset ExpiresAt { get; set; }
-
- public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
-}
\ No newline at end of file
diff --git a/Moonlight.ApiServer/Database/Entities/Theme.cs b/Moonlight.ApiServer/Database/Entities/Theme.cs
deleted file mode 100644
index 22f1ca04..00000000
--- a/Moonlight.ApiServer/Database/Entities/Theme.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Moonlight.ApiServer.Models;
-
-namespace Moonlight.ApiServer.Database.Entities;
-
-public class Theme
-{
- public int Id { get; set; }
-
- public bool IsEnabled { get; set; }
-
- public string Name { get; set; }
- public string Author { get; set; }
- public string Version { get; set; }
-
- public string? UpdateUrl { get; set; }
- public string? DonateUrl { get; set; }
-
- public ApplicationTheme Content { get; set; }
-}
\ No newline at end of file
diff --git a/Moonlight.ApiServer/Database/Entities/User.cs b/Moonlight.ApiServer/Database/Entities/User.cs
deleted file mode 100644
index 4a09c19d..00000000
--- a/Moonlight.ApiServer/Database/Entities/User.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Moonlight.ApiServer.Database.Entities;
-
-public class User
-{
- public int Id { get; set; }
-
- public string Username { get; set; }
- public string Email { get; set; }
- public string Password { get; set; }
- public DateTimeOffset TokenValidTimestamp { get; set; } = DateTimeOffset.MinValue;
- public string[] Permissions { get; set; } = [];
-}
\ No newline at end of file
diff --git a/Moonlight.ApiServer/Database/Migrations/20250919201409_RecreatedMigrationsForChangeOfSchema.Designer.cs b/Moonlight.ApiServer/Database/Migrations/20250919201409_RecreatedMigrationsForChangeOfSchema.Designer.cs
deleted file mode 100644
index 9c9d9577..00000000
--- a/Moonlight.ApiServer/Database/Migrations/20250919201409_RecreatedMigrationsForChangeOfSchema.Designer.cs
+++ /dev/null
@@ -1,565 +0,0 @@
-//
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Moonlight.ApiServer.Database;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace Moonlight.ApiServer.Database.Migrations
-{
- [DbContext(typeof(CoreDataContext))]
- [Migration("20250919201409_RecreatedMigrationsForChangeOfSchema")]
- partial class RecreatedMigrationsForChangeOfSchema
- {
- ///
- protected override void BuildTargetModel(ModelBuilder modelBuilder)
- {
-#pragma warning disable 612, 618
- modelBuilder
- .HasDefaultSchema("core")
- .HasAnnotation("ProductVersion", "9.0.8")
- .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
- NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("bigint");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Key")
- .IsRequired()
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Value")
- .HasColumnType("bigint");
-
- b.HasKey("Id");
-
- b.HasIndex("ExpireAt");
-
- b.HasIndex("Key", "Value");
-
- b.ToTable("HangfireCounter", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
- {
- b.Property("Key")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Field")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Value")
- .HasColumnType("text");
-
- b.HasKey("Key", "Field");
-
- b.HasIndex("ExpireAt");
-
- b.ToTable("HangfireHash", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("bigint");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("CreatedAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("InvocationData")
- .IsRequired()
- .HasColumnType("text");
-
- b.Property("StateId")
- .HasColumnType("bigint");
-
- b.Property("StateName")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.HasKey("Id");
-
- b.HasIndex("ExpireAt");
-
- b.HasIndex("StateId");
-
- b.HasIndex("StateName");
-
- b.ToTable("HangfireJob", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
- {
- b.Property("JobId")
- .HasColumnType("bigint");
-
- b.Property("Name")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Value")
- .HasColumnType("text");
-
- b.HasKey("JobId", "Name");
-
- b.ToTable("HangfireJobParameter", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
- {
- b.Property("Key")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Position")
- .HasColumnType("integer");
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Value")
- .HasColumnType("text");
-
- b.HasKey("Key", "Position");
-
- b.HasIndex("ExpireAt");
-
- b.ToTable("HangfireList", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
- {
- b.Property("Id")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("AcquiredAt")
- .HasColumnType("timestamp with time zone");
-
- b.HasKey("Id");
-
- b.ToTable("HangfireLock", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("bigint");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("FetchedAt")
- .IsConcurrencyToken()
- .HasColumnType("timestamp with time zone");
-
- b.Property("JobId")
- .HasColumnType("bigint");
-
- b.Property("Queue")
- .IsRequired()
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.HasKey("Id");
-
- b.HasIndex("JobId");
-
- b.HasIndex("Queue", "FetchedAt");
-
- b.ToTable("HangfireQueuedJob", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
- {
- b.Property("Id")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Heartbeat")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Queues")
- .IsRequired()
- .HasColumnType("text");
-
- b.Property("StartedAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("WorkerCount")
- .HasColumnType("integer");
-
- b.HasKey("Id");
-
- b.HasIndex("Heartbeat");
-
- b.ToTable("HangfireServer", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
- {
- b.Property("Key")
- .HasMaxLength(100)
- .HasColumnType("character varying(100)");
-
- b.Property("Value")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Score")
- .HasColumnType("double precision");
-
- b.HasKey("Key", "Value");
-
- b.HasIndex("ExpireAt");
-
- b.HasIndex("Key", "Score");
-
- b.ToTable("HangfireSet", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("bigint");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("CreatedAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Data")
- .IsRequired()
- .HasColumnType("text");
-
- b.Property("JobId")
- .HasColumnType("bigint");
-
- b.Property("Name")
- .IsRequired()
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Reason")
- .HasColumnType("text");
-
- b.HasKey("Id");
-
- b.HasIndex("JobId");
-
- b.ToTable("HangfireState", "core");
- });
-
- modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("CreatedAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Description")
- .IsRequired()
- .HasColumnType("text");
-
- b.Property("ExpiresAt")
- .HasColumnType("timestamp with time zone");
-
- b.PrimitiveCollection("Permissions")
- .IsRequired()
- .HasColumnType("text[]");
-
- b.HasKey("Id");
-
- b.ToTable("ApiKeys", "core");
- });
-
- modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("Author")
- .IsRequired()
- .HasColumnType("text");
-
- b.Property("DonateUrl")
- .HasColumnType("text");
-
- b.Property("IsEnabled")
- .HasColumnType("boolean");
-
- b.Property("Name")
- .IsRequired()
- .HasColumnType("text");
-
- b.Property("UpdateUrl")
- .HasColumnType("text");
-
- b.Property("Version")
- .IsRequired()
- .HasColumnType("text");
-
- b.HasKey("Id");
-
- b.ToTable("Themes", "core");
- });
-
- modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("Email")
- .IsRequired()
- .HasColumnType("text");
-
- b.Property("Password")
- .IsRequired()
- .HasColumnType("text");
-
- b.PrimitiveCollection("Permissions")
- .IsRequired()
- .HasColumnType("text[]");
-
- b.Property("TokenValidTimestamp")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Username")
- .IsRequired()
- .HasColumnType("text");
-
- b.HasKey("Id");
-
- b.ToTable("Users", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
- {
- b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State")
- .WithMany()
- .HasForeignKey("StateId");
-
- b.Navigation("State");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
- {
- b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
- .WithMany("Parameters")
- .HasForeignKey("JobId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired();
-
- b.Navigation("Job");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
- {
- b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
- .WithMany("QueuedJobs")
- .HasForeignKey("JobId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired();
-
- b.Navigation("Job");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
- {
- b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
- .WithMany("States")
- .HasForeignKey("JobId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired();
-
- b.Navigation("Job");
- });
-
- modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
- {
- b.OwnsOne("Moonlight.ApiServer.Models.ApplicationTheme", "Content", b1 =>
- {
- b1.Property("ThemeId")
- .HasColumnType("integer");
-
- b1.Property("Border")
- .HasColumnType("real");
-
- b1.Property("ColorAccent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorAccentContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorBackground")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorBase100")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorBase150")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorBase200")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorBase250")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorBase300")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorBaseContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorError")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorErrorContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorInfo")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorInfoContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorNeutral")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorNeutralContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorPrimary")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorPrimaryContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorSecondary")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorSecondaryContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorSuccess")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorSuccessContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorWarning")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("ColorWarningContent")
- .IsRequired()
- .HasColumnType("text");
-
- b1.Property("Depth")
- .HasColumnType("integer");
-
- b1.Property("Noise")
- .HasColumnType("integer");
-
- b1.Property("RadiusBox")
- .HasColumnType("real");
-
- b1.Property("RadiusField")
- .HasColumnType("real");
-
- b1.Property("RadiusSelector")
- .HasColumnType("real");
-
- b1.Property("SizeField")
- .HasColumnType("real");
-
- b1.Property("SizeSelector")
- .HasColumnType("real");
-
- b1.HasKey("ThemeId");
-
- b1.ToTable("Themes", "core");
-
- b1.ToJson("Content");
-
- b1.WithOwner()
- .HasForeignKey("ThemeId");
- });
-
- b.Navigation("Content")
- .IsRequired();
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
- {
- b.Navigation("Parameters");
-
- b.Navigation("QueuedJobs");
-
- b.Navigation("States");
- });
-#pragma warning restore 612, 618
- }
- }
-}
diff --git a/Moonlight.ApiServer/Database/Migrations/20250919201409_RecreatedMigrationsForChangeOfSchema.cs b/Moonlight.ApiServer/Database/Migrations/20250919201409_RecreatedMigrationsForChangeOfSchema.cs
deleted file mode 100644
index 5330cca4..00000000
--- a/Moonlight.ApiServer/Database/Migrations/20250919201409_RecreatedMigrationsForChangeOfSchema.cs
+++ /dev/null
@@ -1,399 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace Moonlight.ApiServer.Database.Migrations
-{
- ///
- public partial class RecreatedMigrationsForChangeOfSchema : Migration
- {
- ///
- protected override void Up(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.EnsureSchema(
- name: "core");
-
- migrationBuilder.CreateTable(
- name: "ApiKeys",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "integer", nullable: false)
- .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
- Description = table.Column(type: "text", nullable: false),
- Permissions = table.Column(type: "text[]", nullable: false),
- ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false),
- CreatedAt = table.Column(type: "timestamp with time zone", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_ApiKeys", x => x.Id);
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireCounter",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "bigint", nullable: false)
- .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
- Key = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- Value = table.Column(type: "bigint", nullable: false),
- ExpireAt = table.Column(type: "timestamp with time zone", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireCounter", x => x.Id);
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireHash",
- schema: "core",
- columns: table => new
- {
- Key = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- Field = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- Value = table.Column(type: "text", nullable: true),
- ExpireAt = table.Column(type: "timestamp with time zone", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireHash", x => new { x.Key, x.Field });
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireList",
- schema: "core",
- columns: table => new
- {
- Key = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- Position = table.Column(type: "integer", nullable: false),
- Value = table.Column(type: "text", nullable: true),
- ExpireAt = table.Column(type: "timestamp with time zone", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireList", x => new { x.Key, x.Position });
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireLock",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- AcquiredAt = table.Column(type: "timestamp with time zone", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireLock", x => x.Id);
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireServer",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- StartedAt = table.Column(type: "timestamp with time zone", nullable: false),
- Heartbeat = table.Column(type: "timestamp with time zone", nullable: false),
- WorkerCount = table.Column(type: "integer", nullable: false),
- Queues = table.Column(type: "text", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireServer", x => x.Id);
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireSet",
- schema: "core",
- columns: table => new
- {
- Key = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
- Value = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- Score = table.Column(type: "double precision", nullable: false),
- ExpireAt = table.Column(type: "timestamp with time zone", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireSet", x => new { x.Key, x.Value });
- });
-
- migrationBuilder.CreateTable(
- name: "Themes",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "integer", nullable: false)
- .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
- IsEnabled = table.Column(type: "boolean", nullable: false),
- Name = table.Column(type: "text", nullable: false),
- Author = table.Column(type: "text", nullable: false),
- Version = table.Column(type: "text", nullable: false),
- UpdateUrl = table.Column(type: "text", nullable: true),
- DonateUrl = table.Column(type: "text", nullable: true),
- Content = table.Column(type: "jsonb", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_Themes", x => x.Id);
- });
-
- migrationBuilder.CreateTable(
- name: "Users",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "integer", nullable: false)
- .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
- Username = table.Column(type: "text", nullable: false),
- Email = table.Column(type: "text", nullable: false),
- Password = table.Column(type: "text", nullable: false),
- TokenValidTimestamp = table.Column(type: "timestamp with time zone", nullable: false),
- Permissions = table.Column(type: "text[]", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_Users", x => x.Id);
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireJob",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "bigint", nullable: false)
- .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
- CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
- StateId = table.Column(type: "bigint", nullable: true),
- StateName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true),
- ExpireAt = table.Column(type: "timestamp with time zone", nullable: true),
- InvocationData = table.Column(type: "text", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireJob", x => x.Id);
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireJobParameter",
- schema: "core",
- columns: table => new
- {
- JobId = table.Column(type: "bigint", nullable: false),
- Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- Value = table.Column(type: "text", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireJobParameter", x => new { x.JobId, x.Name });
- table.ForeignKey(
- name: "FK_HangfireJobParameter_HangfireJob_JobId",
- column: x => x.JobId,
- principalSchema: "core",
- principalTable: "HangfireJob",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireQueuedJob",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "bigint", nullable: false)
- .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
- JobId = table.Column(type: "bigint", nullable: false),
- Queue = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- FetchedAt = table.Column(type: "timestamp with time zone", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireQueuedJob", x => x.Id);
- table.ForeignKey(
- name: "FK_HangfireQueuedJob_HangfireJob_JobId",
- column: x => x.JobId,
- principalSchema: "core",
- principalTable: "HangfireJob",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
-
- migrationBuilder.CreateTable(
- name: "HangfireState",
- schema: "core",
- columns: table => new
- {
- Id = table.Column(type: "bigint", nullable: false)
- .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
- JobId = table.Column(type: "bigint", nullable: false),
- Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
- Reason = table.Column(type: "text", nullable: true),
- CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
- Data = table.Column(type: "text", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_HangfireState", x => x.Id);
- table.ForeignKey(
- name: "FK_HangfireState_HangfireJob_JobId",
- column: x => x.JobId,
- principalSchema: "core",
- principalTable: "HangfireJob",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireCounter_ExpireAt",
- schema: "core",
- table: "HangfireCounter",
- column: "ExpireAt");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireCounter_Key_Value",
- schema: "core",
- table: "HangfireCounter",
- columns: new[] { "Key", "Value" });
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireHash_ExpireAt",
- schema: "core",
- table: "HangfireHash",
- column: "ExpireAt");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireJob_ExpireAt",
- schema: "core",
- table: "HangfireJob",
- column: "ExpireAt");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireJob_StateId",
- schema: "core",
- table: "HangfireJob",
- column: "StateId");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireJob_StateName",
- schema: "core",
- table: "HangfireJob",
- column: "StateName");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireList_ExpireAt",
- schema: "core",
- table: "HangfireList",
- column: "ExpireAt");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireQueuedJob_JobId",
- schema: "core",
- table: "HangfireQueuedJob",
- column: "JobId");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireQueuedJob_Queue_FetchedAt",
- schema: "core",
- table: "HangfireQueuedJob",
- columns: new[] { "Queue", "FetchedAt" });
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireServer_Heartbeat",
- schema: "core",
- table: "HangfireServer",
- column: "Heartbeat");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireSet_ExpireAt",
- schema: "core",
- table: "HangfireSet",
- column: "ExpireAt");
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireSet_Key_Score",
- schema: "core",
- table: "HangfireSet",
- columns: new[] { "Key", "Score" });
-
- migrationBuilder.CreateIndex(
- name: "IX_HangfireState_JobId",
- schema: "core",
- table: "HangfireState",
- column: "JobId");
-
- migrationBuilder.AddForeignKey(
- name: "FK_HangfireJob_HangfireState_StateId",
- schema: "core",
- table: "HangfireJob",
- column: "StateId",
- principalSchema: "core",
- principalTable: "HangfireState",
- principalColumn: "Id");
- }
-
- ///
- protected override void Down(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.DropForeignKey(
- name: "FK_HangfireJob_HangfireState_StateId",
- schema: "core",
- table: "HangfireJob");
-
- migrationBuilder.DropTable(
- name: "ApiKeys",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireCounter",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireHash",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireJobParameter",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireList",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireLock",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireQueuedJob",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireServer",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireSet",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "Themes",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "Users",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireState",
- schema: "core");
-
- migrationBuilder.DropTable(
- name: "HangfireJob",
- schema: "core");
- }
- }
-}
diff --git a/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs b/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs
deleted file mode 100644
index 540e66c1..00000000
--- a/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs
+++ /dev/null
@@ -1,562 +0,0 @@
-//
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Moonlight.ApiServer.Database;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace Moonlight.ApiServer.Database.Migrations
-{
- [DbContext(typeof(CoreDataContext))]
- partial class CoreDataContextModelSnapshot : ModelSnapshot
- {
- protected override void BuildModel(ModelBuilder modelBuilder)
- {
-#pragma warning disable 612, 618
- modelBuilder
- .HasDefaultSchema("core")
- .HasAnnotation("ProductVersion", "9.0.8")
- .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
- NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("bigint");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Key")
- .IsRequired()
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Value")
- .HasColumnType("bigint");
-
- b.HasKey("Id");
-
- b.HasIndex("ExpireAt");
-
- b.HasIndex("Key", "Value");
-
- b.ToTable("HangfireCounter", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
- {
- b.Property("Key")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Field")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Value")
- .HasColumnType("text");
-
- b.HasKey("Key", "Field");
-
- b.HasIndex("ExpireAt");
-
- b.ToTable("HangfireHash", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("bigint");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("CreatedAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("InvocationData")
- .IsRequired()
- .HasColumnType("text");
-
- b.Property("StateId")
- .HasColumnType("bigint");
-
- b.Property("StateName")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.HasKey("Id");
-
- b.HasIndex("ExpireAt");
-
- b.HasIndex("StateId");
-
- b.HasIndex("StateName");
-
- b.ToTable("HangfireJob", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
- {
- b.Property("JobId")
- .HasColumnType("bigint");
-
- b.Property("Name")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Value")
- .HasColumnType("text");
-
- b.HasKey("JobId", "Name");
-
- b.ToTable("HangfireJobParameter", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
- {
- b.Property("Key")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Position")
- .HasColumnType("integer");
-
- b.Property("ExpireAt")
- .HasColumnType("timestamp with time zone");
-
- b.Property("Value")
- .HasColumnType("text");
-
- b.HasKey("Key", "Position");
-
- b.HasIndex("ExpireAt");
-
- b.ToTable("HangfireList", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
- {
- b.Property("Id")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("AcquiredAt")
- .HasColumnType("timestamp with time zone");
-
- b.HasKey("Id");
-
- b.ToTable("HangfireLock", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("bigint");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("FetchedAt")
- .IsConcurrencyToken()
- .HasColumnType("timestamp with time zone");
-
- b.Property("JobId")
- .HasColumnType("bigint");
-
- b.Property("Queue")
- .IsRequired()
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.HasKey("Id");
-
- b.HasIndex("JobId");
-
- b.HasIndex("Queue", "FetchedAt");
-
- b.ToTable("HangfireQueuedJob", "core");
- });
-
- modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
- {
- b.Property("Id")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property("Heartbeat")
- .HasColumnType("timestamp with time zone");
-
- b.Property