19 Commits

Author SHA1 Message Date
Masu Baumgartner
39a1bdeb5d Merge pull request #373 from Moonlight-Panel/v1_FixWingsContentLenght
Fix Content-Lenght for wings
2024-04-01 12:46:30 +02:00
Masu Baumgartner
18f107e485 A possible hotfix 2024-04-01 10:45:01 +00:00
Masu Baumgartner
b56b94041f Update README.md 2024-03-04 11:02:32 +01:00
Marcel Baumgartner
1a3b339983 Merge pull request #369 from NaysKutzu/patch-1
Update README.md
2024-02-09 14:22:39 +01:00
NaysKutzu
106af71c17 Update README.md
Update the logo because it won't be displayed due to your captcha on the page
2024-02-09 14:20:46 +01:00
Marcel Baumgartner
cb1da9f5cd Removed old license 2024-02-06 21:38:33 +01:00
Marcel Baumgartner
743b0b1c1b Updated license 2024-02-06 21:37:32 +01:00
Marcel Baumgartner
8fe695f0cf Merge pull request #367 from gOOvER/issuetemplates
added issue templates
2024-02-03 09:09:09 +01:00
Marcel Baumgartner
344ea046f2 Merge pull request #366 from gOOvER/dependabot
added dependabot.yml to autocheck action updates
2024-02-03 09:08:57 +01:00
Marcel Baumgartner
73a014500d Merge pull request #365 from gOOvER/updateworkflow
update workflow files to latest releases
2024-02-03 09:08:38 +01:00
gOOvER
d3f8ba5434 added issue templates 2024-02-03 08:44:05 +01:00
gOOvER
c068679f9b added dependabot.yml to autocheck action updates 2024-02-03 08:27:36 +01:00
gOOvER
9dfba1e6c4 update workflow files to latest releases 2024-02-03 08:24:07 +01:00
Marcel Baumgartner
27f9ca551c Create FUNDING.yml 2024-01-31 10:35:55 +01:00
Marcel Baumgartner
4d69e8bacb Merge pull request #361 from Dannyx1604/patch-1
Update README.md (grammar check)
2024-01-25 21:43:00 +01:00
Dannyx
ed2d33c69b Update README.md (grammar check) 2024-01-25 21:39:48 +01:00
Marcel Baumgartner
d880189f2b Hotfix for WingsServerConverter 2024-01-12 19:45:18 +01:00
Marcel Baumgartner
9221a66597 Merge pull request #324 from Moonlight-Panel/FixLetsEncrypt
Added additional logs for lets encrypt certificates
2023-10-13 23:02:45 +02:00
Marcel Baumgartner
e1c4f8a2a9 Added additional logs for lets encrypt certificates 2023-10-13 23:02:23 +02:00
818 changed files with 148435 additions and 15311 deletions

View File

@@ -0,0 +1,20 @@
{
"name": "C# (.NET)",
"image": "mcr.microsoft.com/devcontainers/dotnet:0-6.0",
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"streetsidesoftware.code-spell-checker"
]
}
},
"portsAttributes": {
"5118": {
"label": "Moonlight",
"onAutoForward": "notify"
}
}
}

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

8
.gitattributes vendored
View File

@@ -1,2 +1,10 @@
# Auto detect text files and perform LF normalization
* text=auto
Moonlight/wwwroot/** linguist-vendored
Moonlight/wwwroot/assets/js/scripts.bundle.js linguist-vendored
Moonlight/wwwroot/assets/js/widgets.bundle.js linguist-vendored
Moonlight/wwwroot/assets/js/theme.js linguist-vendored
Moonlight/wwwroot/assets/css/boxicons.min.css linguist-vendored
Moonlight/wwwroot/assets/css/style.bundle.css linguist-vendored
Moonlight/wwwroot/assets/plugins/** linguist-vendored
Moonlight/wwwroot/assets/fonts/** linguist-vendored

View File

@@ -33,7 +33,7 @@ body:
attributes:
label: Panel Version
description: Version number of your Panel (latest is not a version)
placeholder: v2 EA
placeholder: 1.4.0
validations:
required: true
@@ -42,7 +42,16 @@ body:
attributes:
label: Daemon Version
description: Version number of your Daemon (latest is not a version)
placeholder: v2 EA
placeholder: 1.4.2
validations:
required: true
- type: input
id: wings-version
attributes:
label: Wings Version
description: Version number of your Wings (latest is not a version)
placeholder: 1.4.2
validations:
required: true
@@ -84,4 +93,4 @@ body:
- 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
required: true

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain Github workflows
- package-ecosystem: "github-actions"
directory: "/" # Must be set to / although they're located in .github/workflows
schedule:
interval: "daily"

View File

@@ -0,0 +1,27 @@
name: Canary Docker Build
on:
workflow_dispatch:
pull_request:
types:
- closed
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login into docker hub
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PW }}
- name: Build and Push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Moonlight/Dockerfile
push: true
tags: moonlightpanel/moonlight:canary
platforms: linux/amd64,linux/arm64

View File

@@ -1,39 +0,0 @@
name: Build and Publish NuGet Package
on:
push:
branches: [ v2_ChangeArchitecture,v2.1 ]
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

View File

@@ -0,0 +1,25 @@
name: Release Docker Build
on:
workflow_dispatch:
release:
types: [ published ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login into docker hub
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PW }}
- name: Build and Push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Moonlight/Dockerfile
push: true
tags: moonlightpanel/moonlight:beta
platforms: linux/amd64,linux/arm64

18
.github/workflows/test-docker-build.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Test Build
on:
push:
branches: [ "main" ]
pull_request:
types:
- closed
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t moonlightpanel/moonlight:${{ github.sha }} -f Moonlight/Dockerfile .

469
.gitignore vendored
View File

@@ -1,434 +1,45 @@
## 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
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.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
Common IntelliJ Platform excludes
# User specific
**/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
**/.idea/httpRequests/
# 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
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Rider
# Rider auto-generates .iml files, and contentModel.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
Moonlight/[Bb]in/
Moonlight/[Oo]bj/
Moonlight/_UpgradeReport_Files/
Moonlight/[Pp]ackages/
*.suo
*.user
.vs/
[Bb]in/
[Oo]bj/
_UpgradeReport_Files/
[Pp]ackages/
Thumbs.db
Desktop.ini
.DS_Store
.idea/.idea.Moonlight/.idea/discord.xml
# Moonlight
storage/
**/.idea/**
style.min.css
# Build script for nuget packages
finalPackages/
nupkgs/
# Scripts
**/bin/**
**/obj/**
Moonlight/publish.ps1
Moonlight/version

13
.idea/.idea.Moonlight/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/projectSettingsUpdater.xml
/modules.xml
/.idea.Moonlight.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EfCoreCommonOptions">
<option name="migrationsToStartupProjects">
<map>
<entry key="16141d00-a997-4ba6-b0dc-af6f4712613a" value="16141d00-a997-4ba6-b0dc-af6f4712613a" />
</map>
</option>
<option name="solutionLevelOptions">
<map>
<entry key="migrationsProject" value="16141d00-a997-4ba6-b0dc-af6f4712613a" />
<entry key="startupProject" value="16141d00-a997-4ba6-b0dc-af6f4712613a" />
</map>
</option>
<option name="startupToMigrationsProjects">
<map>
<entry key="16141d00-a997-4ba6-b0dc-af6f4712613a" value="16141d00-a997-4ba6-b0dc-af6f4712613a" />
</map>
</option>
</component>
</project>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EfCoreDialogsState">
<option name="keyValueStorage">
<map>
<entry key="Common:16141d00-a997-4ba6-b0dc-af6f4712613a:dbContext" value="Moonlight.App.Database.DataContext" />
<entry key="Common:buildConfiguration" value="Debug" />
<entry key="Common:noBuild" value="false" />
<entry key="Common:outputFolder" value="App/Database/Migrations" />
<entry key="Common:useDefaultConnection" value="true" />
</map>
</option>
</component>
</project>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

6
.idea/.idea.Moonlight/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,3 @@
<values>
<value name="mac.address" type="string">a6a05ab0b1614c08281b54fc3b3339170b0f57a5e246c20b7393333dfa28f8f1</value>
</values>

View File

@@ -0,0 +1,3 @@
<values>
<value name="StillAlive" type="qword">133564417297189136</value>
</values>

View File

@@ -0,0 +1,3 @@
<values>
<value name="MachineId" type="string">{A6A05AB0-B161-4C08-281B-54FC3B333917}</value>
</values>

35
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,35 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md.
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Moonlight/bin/Debug/net6.0/Moonlight.dll",
"args": [],
"cwd": "${workspaceFolder}/Moonlight",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

41
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Moonlight.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/Moonlight.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/Moonlight.sln"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.ApiServer\Moonlight.ApiServer.csproj" />
<ProjectReference Include="..\Moonlight.Client.Runtime\Moonlight.Client.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.8" />
</ItemGroup>
<Import Project="Plugins.props" />
</Project>

View File

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

View File

@@ -1,4 +0,0 @@
<Project>
<ItemGroup>
</ItemGroup>
</Project>

View File

@@ -1,35 +0,0 @@
using Moonlight.ApiServer.Runtime;
using Moonlight.ApiServer.Startup;
var pluginLoader = new PluginLoader();
pluginLoader.Initialize();
/*
await startup.Run(args, pluginLoader.Instances);
*/
var cs = new Startup();
await cs.Initialize(args, pluginLoader.Instances);
var builder = WebApplication.CreateBuilder(args);
await cs.AddMoonlight(builder);
var app = builder.Build();
await cs.AddMoonlight(app);
// Handle setup of wasm app hosting in the runtime
// so the Moonlight.ApiServer doesn't need the wasm package
if (cs.Configuration.Frontend.EnableHosting)
{
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
}
await app.RunAsync();

View File

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

View File

@@ -1,102 +0,0 @@
using MoonCore.Helpers;
using YamlDotNet.Serialization;
namespace Moonlight.ApiServer.Configuration;
public record AppConfiguration
{
[YamlMember(Description = "The public url your instance should be accessible through")]
public string PublicUrl { get; set; } = "http://localhost:5165";
[YamlMember(Description = "The credentials of the postgres which moonlight should use")]
public DatabaseConfig Database { get; set; } = new();
[YamlMember(Description = "Settings regarding authentication")]
public AuthenticationConfig Authentication { get; set; } = new();
[YamlMember(Description = "These options are only meant for development purposes")]
public DevelopmentConfig Development { get; set; } = new();
public FrontendData Frontend { get; set; } = new();
public KestrelConfig Kestrel { get; set; } = new();
public MetricsData Metrics { get; set; } = new();
public static AppConfiguration CreateEmpty()
{
return new AppConfiguration()
{
// Set arrays as empty here
Kestrel = new()
{
AllowedOrigins = []
}
};
}
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 = "The lifespan of generated user tokens in hours")]
public int TokenDuration { get; set; } = 24 * 10;
[YamlMember(Description = "This enables the use of the local oauth2 provider, so moonlight will use itself as an oauth2 provider")]
public bool EnableLocalOAuth2 { get; set; } = true;
public OAuth2Data OAuth2 { get; set; } = new();
public record OAuth2Data
{
public string Secret { get; set; } = Formatter.GenerateString(32);
public string ClientId { get; set; } = Formatter.GenerateString(8);
public string ClientSecret { get; set; } = Formatter.GenerateString(32);
public string? AuthorizationEndpoint { get; set; }
public string? AccessEndpoint { get; set; }
public string? AuthorizationRedirect { get; set; }
[YamlMember(Description = "This specifies if the first registered user will become an admin automatically. This only works when using local oauth2")]
public bool FirstUserAdmin { get; set; } = true;
}
}
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 MetricsData
{
[YamlMember(Description = "This enables the collecting of metrics and allows access to the /metrics endpoint")]
public bool Enable { get; set; } = false;
[YamlMember(Description = "The interval in which metrics are created, specified in seconds")]
public int Interval { get; set; } = 15;
}
}

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.SingleDb;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Models;
namespace Moonlight.ApiServer.Database;
public class CoreDataContext : DatabaseContext
{
public override string Prefix { get; } = "Core";
public DbSet<User> Users { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Theme> Themes { get; set; }
public CoreDataContext(AppConfiguration configuration)
{
Options = new()
{
Host = configuration.Database.Host,
Port = configuration.Database.Port,
Username = configuration.Database.Username,
Password = configuration.Database.Password,
Database = configuration.Database.Database
};
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Ignore<ApplicationTheme>();
modelBuilder.Entity<Theme>()
.OwnsOne(x => x.Content, builder =>
{
builder.ToJson();
});
}
}

View File

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

View File

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

View File

@@ -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; } = [];
}

View File

@@ -1,90 +0,0 @@
// <auto-generated />
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("20250226080942_AddedUsersAndApiKey")]
partial class AddedUsersAndApiKey
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Secret")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,59 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedUsersAndApiKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Core_ApiKeys",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Secret = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
PermissionsJson = table.Column<string>(type: "jsonb", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Core_ApiKeys", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Core_Users",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false),
TokenValidTimestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
PermissionsJson = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Core_Users", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Core_ApiKeys");
migrationBuilder.DropTable(
name: "Core_Users");
}
}
}

View File

@@ -1,89 +0,0 @@
// <auto-generated />
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("20250314095412_ModifiedApiKeyEntity")]
partial class ModifiedApiKeyEntity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,41 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class ModifiedApiKeyEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Secret",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Core_ApiKeys",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<string>(
name: "Secret",
table: "Core_ApiKeys",
type: "text",
nullable: false,
defaultValue: "");
}
}
}

View File

@@ -1,393 +0,0 @@
// <auto-generated />
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("20250405172522_AddedHangfireTables")]
partial class AddedHangfireTables
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long>("Value")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Value");
b.ToTable("HangfireCounter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Field")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Field");
b.HasIndex("ExpireAt");
b.ToTable("HangfireHash");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvocationData")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("StateId")
.HasColumnType("bigint");
b.Property<string>("StateName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("StateId");
b.HasIndex("StateName");
b.ToTable("HangfireJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("JobId", "Name");
b.ToTable("HangfireJobParameter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Position");
b.HasIndex("ExpireAt");
b.ToTable("HangfireList");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("AcquiredAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("HangfireLock");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("FetchedAt")
.IsConcurrencyToken()
.HasColumnType("timestamp with time zone");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Queue")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("Queue", "FetchedAt");
b.ToTable("HangfireQueuedJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("Heartbeat")
.HasColumnType("timestamp with time zone");
b.Property<string>("Queues")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("WorkerCount")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Heartbeat");
b.ToTable("HangfireServer");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
{
b.Property<string>("Key")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Value")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<double>("Score")
.HasColumnType("double precision");
b.HasKey("Key", "Value");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Score");
b.ToTable("HangfireSet");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("text");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Reason")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("JobId");
b.ToTable("HangfireState");
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
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("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Navigation("Parameters");
b.Navigation("QueuedJobs");
b.Navigation("States");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,290 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedHangfireTables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "HangfireCounter",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Value = table.Column<long>(type: "bigint", nullable: false),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireCounter", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HangfireHash",
columns: table => new
{
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Field = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Value = table.Column<string>(type: "text", nullable: true),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireHash", x => new { x.Key, x.Field });
});
migrationBuilder.CreateTable(
name: "HangfireList",
columns: table => new
{
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Position = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<string>(type: "text", nullable: true),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireList", x => new { x.Key, x.Position });
});
migrationBuilder.CreateTable(
name: "HangfireLock",
columns: table => new
{
Id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
AcquiredAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireLock", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HangfireServer",
columns: table => new
{
Id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
StartedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Heartbeat = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
WorkerCount = table.Column<int>(type: "integer", nullable: false),
Queues = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireServer", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HangfireSet",
columns: table => new
{
Key = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Value = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Score = table.Column<double>(type: "double precision", nullable: false),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireSet", x => new { x.Key, x.Value });
});
migrationBuilder.CreateTable(
name: "HangfireJob",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
StateId = table.Column<long>(type: "bigint", nullable: true),
StateName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
InvocationData = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireJob", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HangfireJobParameter",
columns: table => new
{
JobId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Value = table.Column<string>(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,
principalTable: "HangfireJob",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "HangfireQueuedJob",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
JobId = table.Column<long>(type: "bigint", nullable: false),
Queue = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
FetchedAt = table.Column<DateTime>(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,
principalTable: "HangfireJob",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "HangfireState",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
JobId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Reason = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Data = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireState", x => x.Id);
table.ForeignKey(
name: "FK_HangfireState_HangfireJob_JobId",
column: x => x.JobId,
principalTable: "HangfireJob",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_HangfireCounter_ExpireAt",
table: "HangfireCounter",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireCounter_Key_Value",
table: "HangfireCounter",
columns: new[] { "Key", "Value" });
migrationBuilder.CreateIndex(
name: "IX_HangfireHash_ExpireAt",
table: "HangfireHash",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireJob_ExpireAt",
table: "HangfireJob",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireJob_StateId",
table: "HangfireJob",
column: "StateId");
migrationBuilder.CreateIndex(
name: "IX_HangfireJob_StateName",
table: "HangfireJob",
column: "StateName");
migrationBuilder.CreateIndex(
name: "IX_HangfireList_ExpireAt",
table: "HangfireList",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireQueuedJob_JobId",
table: "HangfireQueuedJob",
column: "JobId");
migrationBuilder.CreateIndex(
name: "IX_HangfireQueuedJob_Queue_FetchedAt",
table: "HangfireQueuedJob",
columns: new[] { "Queue", "FetchedAt" });
migrationBuilder.CreateIndex(
name: "IX_HangfireServer_Heartbeat",
table: "HangfireServer",
column: "Heartbeat");
migrationBuilder.CreateIndex(
name: "IX_HangfireSet_ExpireAt",
table: "HangfireSet",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireSet_Key_Score",
table: "HangfireSet",
columns: new[] { "Key", "Score" });
migrationBuilder.CreateIndex(
name: "IX_HangfireState_JobId",
table: "HangfireState",
column: "JobId");
migrationBuilder.AddForeignKey(
name: "FK_HangfireJob_HangfireState_StateId",
table: "HangfireJob",
column: "StateId",
principalTable: "HangfireState",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_HangfireJob_HangfireState_StateId",
table: "HangfireJob");
migrationBuilder.DropTable(
name: "HangfireCounter");
migrationBuilder.DropTable(
name: "HangfireHash");
migrationBuilder.DropTable(
name: "HangfireJobParameter");
migrationBuilder.DropTable(
name: "HangfireList");
migrationBuilder.DropTable(
name: "HangfireLock");
migrationBuilder.DropTable(
name: "HangfireQueuedJob");
migrationBuilder.DropTable(
name: "HangfireServer");
migrationBuilder.DropTable(
name: "HangfireSet");
migrationBuilder.DropTable(
name: "HangfireState");
migrationBuilder.DropTable(
name: "HangfireJob");
}
}
}

View File

@@ -1,393 +0,0 @@
// <auto-generated />
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("20250712202608_SwitchedToPgArraysForPermissions")]
partial class SwitchedToPgArraysForPermissions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long>("Value")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Value");
b.ToTable("HangfireCounter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Field")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Field");
b.HasIndex("ExpireAt");
b.ToTable("HangfireHash");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvocationData")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("StateId")
.HasColumnType("bigint");
b.Property<string>("StateName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("StateId");
b.HasIndex("StateName");
b.ToTable("HangfireJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("JobId", "Name");
b.ToTable("HangfireJobParameter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Position");
b.HasIndex("ExpireAt");
b.ToTable("HangfireList");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("AcquiredAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("HangfireLock");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("FetchedAt")
.IsConcurrencyToken()
.HasColumnType("timestamp with time zone");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Queue")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("Queue", "FetchedAt");
b.ToTable("HangfireQueuedJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("Heartbeat")
.HasColumnType("timestamp with time zone");
b.Property<string>("Queues")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("WorkerCount")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Heartbeat");
b.ToTable("HangfireServer");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
{
b.Property<string>("Key")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Value")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<double>("Score")
.HasColumnType("double precision");
b.HasKey("Key", "Value");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Score");
b.ToTable("HangfireSet");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("text");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Reason")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("JobId");
b.ToTable("HangfireState");
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
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("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Navigation("Parameters");
b.Navigation("QueuedJobs");
b.Navigation("States");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,62 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class SwitchedToPgArraysForPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PermissionsJson",
table: "Core_Users");
migrationBuilder.DropColumn(
name: "PermissionsJson",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<string[]>(
name: "Permissions",
table: "Core_Users",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "Permissions",
table: "Core_ApiKeys",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Permissions",
table: "Core_Users");
migrationBuilder.DropColumn(
name: "Permissions",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<string>(
name: "PermissionsJson",
table: "Core_Users",
type: "jsonb",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "PermissionsJson",
table: "Core_ApiKeys",
type: "jsonb",
nullable: false,
defaultValue: "");
}
}
}

View File

@@ -1,564 +0,0 @@
// <auto-generated />
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("20250720203346_AddedThemes")]
partial class AddedThemes
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long>("Value")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Value");
b.ToTable("HangfireCounter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Field")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Field");
b.HasIndex("ExpireAt");
b.ToTable("HangfireHash");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvocationData")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("StateId")
.HasColumnType("bigint");
b.Property<string>("StateName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("StateId");
b.HasIndex("StateName");
b.ToTable("HangfireJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("JobId", "Name");
b.ToTable("HangfireJobParameter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Position");
b.HasIndex("ExpireAt");
b.ToTable("HangfireList");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("AcquiredAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("HangfireLock");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("FetchedAt")
.IsConcurrencyToken()
.HasColumnType("timestamp with time zone");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Queue")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("Queue", "FetchedAt");
b.ToTable("HangfireQueuedJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("Heartbeat")
.HasColumnType("timestamp with time zone");
b.Property<string>("Queues")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("WorkerCount")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Heartbeat");
b.ToTable("HangfireServer");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
{
b.Property<string>("Key")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Value")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<double>("Score")
.HasColumnType("double precision");
b.HasKey("Key", "Value");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Score");
b.ToTable("HangfireSet");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("text");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Reason")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("JobId");
b.ToTable("HangfireState");
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DonateUrl")
.HasColumnType("text");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UpdateUrl")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Themes", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
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<int>("ThemeId")
.HasColumnType("integer");
b1.Property<float>("Border")
.HasColumnType("real");
b1.Property<string>("ColorAccent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorAccentContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBackground")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase100")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase150")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase200")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase250")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase300")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBaseContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorError")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorErrorContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorInfo")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorInfoContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorNeutral")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorNeutralContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorPrimary")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorPrimaryContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSecondary")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSecondaryContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSuccess")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSuccessContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorWarning")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorWarningContent")
.IsRequired()
.HasColumnType("text");
b1.Property<float>("Depth")
.HasColumnType("real");
b1.Property<float>("Noise")
.HasColumnType("real");
b1.Property<float>("RadiusBox")
.HasColumnType("real");
b1.Property<float>("RadiusField")
.HasColumnType("real");
b1.Property<float>("RadiusSelector")
.HasColumnType("real");
b1.Property<float>("SizeField")
.HasColumnType("real");
b1.Property<float>("SizeSelector")
.HasColumnType("real");
b1.HasKey("ThemeId");
b1.ToTable("Core_Themes");
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
}
}
}

View File

@@ -1,41 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedThemes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Core_Themes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Author = table.Column<string>(type: "text", nullable: false),
Version = table.Column<string>(type: "text", nullable: false),
UpdateUrl = table.Column<string>(type: "text", nullable: true),
DonateUrl = table.Column<string>(type: "text", nullable: true),
Content = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Core_Themes", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Core_Themes");
}
}
}

View File

@@ -1,260 +0,0 @@
// <auto-generated />
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("20250819195904_RemovedHangfire")]
partial class RemovedHangfire
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DonateUrl")
.HasColumnType("text");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UpdateUrl")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Themes", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
{
b.OwnsOne("Moonlight.ApiServer.Models.ApplicationTheme", "Content", b1 =>
{
b1.Property<int>("ThemeId")
.HasColumnType("integer");
b1.Property<float>("Border")
.HasColumnType("real");
b1.Property<string>("ColorAccent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorAccentContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBackground")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase100")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase150")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase200")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase250")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase300")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBaseContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorError")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorErrorContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorInfo")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorInfoContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorNeutral")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorNeutralContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorPrimary")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorPrimaryContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSecondary")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSecondaryContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSuccess")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSuccessContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorWarning")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorWarningContent")
.IsRequired()
.HasColumnType("text");
b1.Property<int>("Depth")
.HasColumnType("integer");
b1.Property<int>("Noise")
.HasColumnType("integer");
b1.Property<float>("RadiusBox")
.HasColumnType("real");
b1.Property<float>("RadiusField")
.HasColumnType("real");
b1.Property<float>("RadiusSelector")
.HasColumnType("real");
b1.Property<float>("SizeField")
.HasColumnType("real");
b1.Property<float>("SizeSelector")
.HasColumnType("real");
b1.HasKey("ThemeId");
b1.ToTable("Core_Themes");
b1.ToJson("Content");
b1.WithOwner()
.HasForeignKey("ThemeId");
});
b.Navigation("Content")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,290 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class RemovedHangfire : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_HangfireJob_HangfireState_StateId",
table: "HangfireJob");
migrationBuilder.DropTable(
name: "HangfireCounter");
migrationBuilder.DropTable(
name: "HangfireHash");
migrationBuilder.DropTable(
name: "HangfireJobParameter");
migrationBuilder.DropTable(
name: "HangfireList");
migrationBuilder.DropTable(
name: "HangfireLock");
migrationBuilder.DropTable(
name: "HangfireQueuedJob");
migrationBuilder.DropTable(
name: "HangfireServer");
migrationBuilder.DropTable(
name: "HangfireSet");
migrationBuilder.DropTable(
name: "HangfireState");
migrationBuilder.DropTable(
name: "HangfireJob");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "HangfireCounter",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Value = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireCounter", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HangfireHash",
columns: table => new
{
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Field = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireHash", x => new { x.Key, x.Field });
});
migrationBuilder.CreateTable(
name: "HangfireList",
columns: table => new
{
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Position = table.Column<int>(type: "integer", nullable: false),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireList", x => new { x.Key, x.Position });
});
migrationBuilder.CreateTable(
name: "HangfireLock",
columns: table => new
{
Id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
AcquiredAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireLock", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HangfireServer",
columns: table => new
{
Id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Heartbeat = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Queues = table.Column<string>(type: "text", nullable: false),
StartedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
WorkerCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireServer", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HangfireSet",
columns: table => new
{
Key = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Value = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Score = table.Column<double>(type: "double precision", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireSet", x => new { x.Key, x.Value });
});
migrationBuilder.CreateTable(
name: "HangfireJob",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
StateId = table.Column<long>(type: "bigint", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
InvocationData = table.Column<string>(type: "text", nullable: false),
StateName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireJob", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HangfireJobParameter",
columns: table => new
{
JobId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Value = table.Column<string>(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,
principalTable: "HangfireJob",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "HangfireQueuedJob",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
JobId = table.Column<long>(type: "bigint", nullable: false),
FetchedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Queue = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireQueuedJob", x => x.Id);
table.ForeignKey(
name: "FK_HangfireQueuedJob_HangfireJob_JobId",
column: x => x.JobId,
principalTable: "HangfireJob",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "HangfireState",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
JobId = table.Column<long>(type: "bigint", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Data = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Reason = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HangfireState", x => x.Id);
table.ForeignKey(
name: "FK_HangfireState_HangfireJob_JobId",
column: x => x.JobId,
principalTable: "HangfireJob",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_HangfireCounter_ExpireAt",
table: "HangfireCounter",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireCounter_Key_Value",
table: "HangfireCounter",
columns: new[] { "Key", "Value" });
migrationBuilder.CreateIndex(
name: "IX_HangfireHash_ExpireAt",
table: "HangfireHash",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireJob_ExpireAt",
table: "HangfireJob",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireJob_StateId",
table: "HangfireJob",
column: "StateId");
migrationBuilder.CreateIndex(
name: "IX_HangfireJob_StateName",
table: "HangfireJob",
column: "StateName");
migrationBuilder.CreateIndex(
name: "IX_HangfireList_ExpireAt",
table: "HangfireList",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireQueuedJob_JobId",
table: "HangfireQueuedJob",
column: "JobId");
migrationBuilder.CreateIndex(
name: "IX_HangfireQueuedJob_Queue_FetchedAt",
table: "HangfireQueuedJob",
columns: new[] { "Queue", "FetchedAt" });
migrationBuilder.CreateIndex(
name: "IX_HangfireServer_Heartbeat",
table: "HangfireServer",
column: "Heartbeat");
migrationBuilder.CreateIndex(
name: "IX_HangfireSet_ExpireAt",
table: "HangfireSet",
column: "ExpireAt");
migrationBuilder.CreateIndex(
name: "IX_HangfireSet_Key_Score",
table: "HangfireSet",
columns: new[] { "Key", "Score" });
migrationBuilder.CreateIndex(
name: "IX_HangfireState_JobId",
table: "HangfireState",
column: "JobId");
migrationBuilder.AddForeignKey(
name: "FK_HangfireJob_HangfireState_StateId",
table: "HangfireJob",
column: "StateId",
principalTable: "HangfireState",
principalColumn: "Id");
}
}
}

View File

@@ -1,222 +0,0 @@
// <auto-generated />
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(TickerDataContext))]
[Migration("20250819200221_AddedTickerQ")]
partial class AddedTickerQ
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.CronTickerEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Expression")
.HasColumnType("text");
b.Property<string>("Function")
.HasColumnType("text");
b.Property<string>("InitIdentifier")
.HasColumnType("text");
b.Property<byte[]>("Request")
.HasColumnType("bytea");
b.Property<int>("Retries")
.HasColumnType("integer");
b.PrimitiveCollection<int[]>("RetryIntervals")
.HasColumnType("integer[]");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Expression")
.HasDatabaseName("IX_CronTickers_Expression");
b.ToTable("CronTickers", "ticker");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.CronTickerOccurrenceEntity<TickerQ.EntityFrameworkCore.Entities.CronTickerEntity>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("CronTickerId")
.HasColumnType("uuid");
b.Property<long>("ElapsedTime")
.HasColumnType("bigint");
b.Property<string>("Exception")
.HasColumnType("text");
b.Property<DateTime?>("ExecutedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExecutionTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("LockHolder")
.HasColumnType("text");
b.Property<DateTime?>("LockedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("RetryCount")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("CronTickerId")
.HasDatabaseName("IX_CronTickerOccurrence_CronTickerId");
b.HasIndex("ExecutionTime")
.HasDatabaseName("IX_CronTickerOccurrence_ExecutionTime");
b.HasIndex("CronTickerId", "ExecutionTime")
.IsUnique()
.HasDatabaseName("UQ_CronTickerId_ExecutionTime");
b.HasIndex("Status", "ExecutionTime")
.HasDatabaseName("IX_CronTickerOccurrence_Status_ExecutionTime");
b.ToTable("CronTickerOccurrences", "ticker");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.TimeTickerEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("BatchParent")
.HasColumnType("uuid");
b.Property<int?>("BatchRunCondition")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<long>("ElapsedTime")
.HasColumnType("bigint");
b.Property<string>("Exception")
.HasColumnType("text");
b.Property<DateTime?>("ExecutedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExecutionTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Function")
.HasColumnType("text");
b.Property<string>("InitIdentifier")
.HasColumnType("text");
b.Property<string>("LockHolder")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime?>("LockedAt")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("Request")
.HasColumnType("bytea");
b.Property<int>("Retries")
.HasColumnType("integer");
b.Property<int>("RetryCount")
.HasColumnType("integer");
b.PrimitiveCollection<int[]>("RetryIntervals")
.HasColumnType("integer[]");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("BatchParent");
b.HasIndex("ExecutionTime")
.HasDatabaseName("IX_TimeTicker_ExecutionTime");
b.HasIndex("Status", "ExecutionTime")
.HasDatabaseName("IX_TimeTicker_Status_ExecutionTime");
b.ToTable("TimeTickers", "ticker");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.CronTickerOccurrenceEntity<TickerQ.EntityFrameworkCore.Entities.CronTickerEntity>", b =>
{
b.HasOne("TickerQ.EntityFrameworkCore.Entities.CronTickerEntity", "CronTicker")
.WithMany()
.HasForeignKey("CronTickerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CronTicker");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.TimeTickerEntity", b =>
{
b.HasOne("TickerQ.EntityFrameworkCore.Entities.TimeTickerEntity", "ParentJob")
.WithMany("ChildJobs")
.HasForeignKey("BatchParent")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("ParentJob");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.TimeTickerEntity", b =>
{
b.Navigation("ChildJobs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,169 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedTickerQ : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "ticker");
migrationBuilder.CreateTable(
name: "CronTickers",
schema: "ticker",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Expression = table.Column<string>(type: "text", nullable: true),
Request = table.Column<byte[]>(type: "bytea", nullable: true),
Retries = table.Column<int>(type: "integer", nullable: false),
RetryIntervals = table.Column<int[]>(type: "integer[]", nullable: true),
Function = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
InitIdentifier = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CronTickers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "TimeTickers",
schema: "ticker",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
LockHolder = table.Column<string>(type: "text", nullable: true),
Request = table.Column<byte[]>(type: "bytea", nullable: true),
ExecutionTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
LockedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ExecutedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Exception = table.Column<string>(type: "text", nullable: true),
ElapsedTime = table.Column<long>(type: "bigint", nullable: false),
Retries = table.Column<int>(type: "integer", nullable: false),
RetryCount = table.Column<int>(type: "integer", nullable: false),
RetryIntervals = table.Column<int[]>(type: "integer[]", nullable: true),
BatchParent = table.Column<Guid>(type: "uuid", nullable: true),
BatchRunCondition = table.Column<int>(type: "integer", nullable: true),
Function = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
InitIdentifier = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TimeTickers", x => x.Id);
table.ForeignKey(
name: "FK_TimeTickers_TimeTickers_BatchParent",
column: x => x.BatchParent,
principalSchema: "ticker",
principalTable: "TimeTickers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "CronTickerOccurrences",
schema: "ticker",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
LockHolder = table.Column<string>(type: "text", nullable: true),
ExecutionTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CronTickerId = table.Column<Guid>(type: "uuid", nullable: false),
LockedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ExecutedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Exception = table.Column<string>(type: "text", nullable: true),
ElapsedTime = table.Column<long>(type: "bigint", nullable: false),
RetryCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CronTickerOccurrences", x => x.Id);
table.ForeignKey(
name: "FK_CronTickerOccurrences_CronTickers_CronTickerId",
column: x => x.CronTickerId,
principalSchema: "ticker",
principalTable: "CronTickers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CronTickerOccurrence_CronTickerId",
schema: "ticker",
table: "CronTickerOccurrences",
column: "CronTickerId");
migrationBuilder.CreateIndex(
name: "IX_CronTickerOccurrence_ExecutionTime",
schema: "ticker",
table: "CronTickerOccurrences",
column: "ExecutionTime");
migrationBuilder.CreateIndex(
name: "IX_CronTickerOccurrence_Status_ExecutionTime",
schema: "ticker",
table: "CronTickerOccurrences",
columns: new[] { "Status", "ExecutionTime" });
migrationBuilder.CreateIndex(
name: "UQ_CronTickerId_ExecutionTime",
schema: "ticker",
table: "CronTickerOccurrences",
columns: new[] { "CronTickerId", "ExecutionTime" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_CronTickers_Expression",
schema: "ticker",
table: "CronTickers",
column: "Expression");
migrationBuilder.CreateIndex(
name: "IX_TimeTicker_ExecutionTime",
schema: "ticker",
table: "TimeTickers",
column: "ExecutionTime");
migrationBuilder.CreateIndex(
name: "IX_TimeTicker_Status_ExecutionTime",
schema: "ticker",
table: "TimeTickers",
columns: new[] { "Status", "ExecutionTime" });
migrationBuilder.CreateIndex(
name: "IX_TimeTickers_BatchParent",
schema: "ticker",
table: "TimeTickers",
column: "BatchParent");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CronTickerOccurrences",
schema: "ticker");
migrationBuilder.DropTable(
name: "TimeTickers",
schema: "ticker");
migrationBuilder.DropTable(
name: "CronTickers",
schema: "ticker");
}
}
}

View File

@@ -1,257 +0,0 @@
// <auto-generated />
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
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DonateUrl")
.HasColumnType("text");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UpdateUrl")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Themes", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
{
b.OwnsOne("Moonlight.ApiServer.Models.ApplicationTheme", "Content", b1 =>
{
b1.Property<int>("ThemeId")
.HasColumnType("integer");
b1.Property<float>("Border")
.HasColumnType("real");
b1.Property<string>("ColorAccent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorAccentContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBackground")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase100")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase150")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase200")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase250")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBase300")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorBaseContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorError")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorErrorContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorInfo")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorInfoContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorNeutral")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorNeutralContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorPrimary")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorPrimaryContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSecondary")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSecondaryContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSuccess")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorSuccessContent")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorWarning")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("ColorWarningContent")
.IsRequired()
.HasColumnType("text");
b1.Property<int>("Depth")
.HasColumnType("integer");
b1.Property<int>("Noise")
.HasColumnType("integer");
b1.Property<float>("RadiusBox")
.HasColumnType("real");
b1.Property<float>("RadiusField")
.HasColumnType("real");
b1.Property<float>("RadiusSelector")
.HasColumnType("real");
b1.Property<float>("SizeField")
.HasColumnType("real");
b1.Property<float>("SizeSelector")
.HasColumnType("real");
b1.HasKey("ThemeId");
b1.ToTable("Core_Themes");
b1.ToJson("Content");
b1.WithOwner()
.HasForeignKey("ThemeId");
});
b.Navigation("Content")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,219 +0,0 @@
// <auto-generated />
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(TickerDataContext))]
partial class TickerDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.CronTickerEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Expression")
.HasColumnType("text");
b.Property<string>("Function")
.HasColumnType("text");
b.Property<string>("InitIdentifier")
.HasColumnType("text");
b.Property<byte[]>("Request")
.HasColumnType("bytea");
b.Property<int>("Retries")
.HasColumnType("integer");
b.PrimitiveCollection<int[]>("RetryIntervals")
.HasColumnType("integer[]");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Expression")
.HasDatabaseName("IX_CronTickers_Expression");
b.ToTable("CronTickers", "ticker");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.CronTickerOccurrenceEntity<TickerQ.EntityFrameworkCore.Entities.CronTickerEntity>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("CronTickerId")
.HasColumnType("uuid");
b.Property<long>("ElapsedTime")
.HasColumnType("bigint");
b.Property<string>("Exception")
.HasColumnType("text");
b.Property<DateTime?>("ExecutedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExecutionTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("LockHolder")
.HasColumnType("text");
b.Property<DateTime?>("LockedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("RetryCount")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("CronTickerId")
.HasDatabaseName("IX_CronTickerOccurrence_CronTickerId");
b.HasIndex("ExecutionTime")
.HasDatabaseName("IX_CronTickerOccurrence_ExecutionTime");
b.HasIndex("CronTickerId", "ExecutionTime")
.IsUnique()
.HasDatabaseName("UQ_CronTickerId_ExecutionTime");
b.HasIndex("Status", "ExecutionTime")
.HasDatabaseName("IX_CronTickerOccurrence_Status_ExecutionTime");
b.ToTable("CronTickerOccurrences", "ticker");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.TimeTickerEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("BatchParent")
.HasColumnType("uuid");
b.Property<int?>("BatchRunCondition")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<long>("ElapsedTime")
.HasColumnType("bigint");
b.Property<string>("Exception")
.HasColumnType("text");
b.Property<DateTime?>("ExecutedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExecutionTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Function")
.HasColumnType("text");
b.Property<string>("InitIdentifier")
.HasColumnType("text");
b.Property<string>("LockHolder")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime?>("LockedAt")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("Request")
.HasColumnType("bytea");
b.Property<int>("Retries")
.HasColumnType("integer");
b.Property<int>("RetryCount")
.HasColumnType("integer");
b.PrimitiveCollection<int[]>("RetryIntervals")
.HasColumnType("integer[]");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("BatchParent");
b.HasIndex("ExecutionTime")
.HasDatabaseName("IX_TimeTicker_ExecutionTime");
b.HasIndex("Status", "ExecutionTime")
.HasDatabaseName("IX_TimeTicker_Status_ExecutionTime");
b.ToTable("TimeTickers", "ticker");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.CronTickerOccurrenceEntity<TickerQ.EntityFrameworkCore.Entities.CronTickerEntity>", b =>
{
b.HasOne("TickerQ.EntityFrameworkCore.Entities.CronTickerEntity", "CronTicker")
.WithMany()
.HasForeignKey("CronTickerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CronTicker");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.TimeTickerEntity", b =>
{
b.HasOne("TickerQ.EntityFrameworkCore.Entities.TimeTickerEntity", "ParentJob")
.WithMany("ChildJobs")
.HasForeignKey("BatchParent")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("ParentJob");
});
modelBuilder.Entity("TickerQ.EntityFrameworkCore.Entities.TimeTickerEntity", b =>
{
b.Navigation("ChildJobs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,31 +0,0 @@
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.SingleDb;
using Moonlight.ApiServer.Configuration;
using TickerQ.EntityFrameworkCore.Configurations;
namespace Moonlight.ApiServer.Database;
public class TickerDataContext : DatabaseContext
{
public override string Prefix => "Ticker";
public TickerDataContext(AppConfiguration configuration)
{
Options = new()
{
Host = configuration.Database.Host,
Port = configuration.Database.Port,
Username = configuration.Database.Username,
Password = configuration.Database.Password,
Database = configuration.Database.Database
};
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply TickerQ entity configurations explicitly
modelBuilder.ApplyConfiguration(new TimeTickerConfigurations());
modelBuilder.ApplyConfiguration(new CronTickerConfigurations());
modelBuilder.ApplyConfiguration(new CronTickerOccurrenceConfigurations());
}
}

View File

@@ -1,33 +0,0 @@
using System.IO.Compression;
using System.Text;
namespace Moonlight.ApiServer.Extensions;
public static class ZipArchiveExtensions
{
public static async Task AddBinary(this ZipArchive archive, string name, byte[] bytes)
{
var entry = archive.CreateEntry(name);
await using var dataStream = entry.Open();
await dataStream.WriteAsync(bytes);
await dataStream.FlushAsync();
}
public static async Task AddText(this ZipArchive archive, string name, string content)
{
var data = Encoding.UTF8.GetBytes(content);
await archive.AddBinary(name, data);
}
public static async Task AddFile(this ZipArchive archive, string name, string path)
{
var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var entry = archive.CreateEntry(name);
await using var dataStream = entry.Open();
await fs.CopyToAsync(dataStream);
await dataStream.FlushAsync();
}
}

View File

@@ -1,146 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
namespace Moonlight.ApiServer.Http.Controllers.Admin.ApiKeys;
[ApiController]
[Route("api/admin/apikeys")]
public class ApiKeysController : Controller
{
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
private readonly ApiKeyService ApiKeyService;
public ApiKeysController(DatabaseRepository<ApiKey> apiKeyRepository, ApiKeyService apiKeyService)
{
ApiKeyRepository = apiKeyRepository;
ApiKeyService = apiKeyService;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.apikeys.get")]
public async Task<IPagedData<ApiKeyResponse>> Get(
[FromQuery] [Range(0, int.MaxValue)] int page,
[FromQuery] [Range(1, 100)] int pageSize
)
{
var count = await ApiKeyRepository.Get().CountAsync();
var apiKeys = await ApiKeyRepository
.Get()
.OrderBy(x => x.Id)
.Skip(page * pageSize)
.Take(pageSize)
.ToArrayAsync();
var mappedApiKey = apiKeys
.Select(x => new ApiKeyResponse()
{
Id = x.Id,
Permissions = x.Permissions,
Description = x.Description,
ExpiresAt = x.ExpiresAt
})
.ToArray();
return new PagedData<ApiKeyResponse>()
{
CurrentPage = page,
Items = mappedApiKey,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
};
}
[HttpGet("{id}")]
[Authorize(Policy = "permissions:admin.apikeys.get")]
public async Task<ApiKeyResponse> GetSingle(int id)
{
var apiKey = await ApiKeyRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404);
return new ApiKeyResponse()
{
Id = apiKey.Id,
Permissions = apiKey.Permissions,
Description = apiKey.Description,
ExpiresAt = apiKey.ExpiresAt
};
}
[HttpPost]
[Authorize(Policy = "permissions:admin.apikeys.create")]
public async Task<CreateApiKeyResponse> Create([FromBody] CreateApiKeyRequest request)
{
var apiKey = new ApiKey()
{
Description = request.Description,
Permissions = request.Permissions,
ExpiresAt = request.ExpiresAt
};
var finalApiKey = await ApiKeyRepository.Add(apiKey);
var response = new CreateApiKeyResponse
{
Id = finalApiKey.Id,
Permissions = finalApiKey.Permissions,
Description = finalApiKey.Description,
ExpiresAt = finalApiKey.ExpiresAt,
Secret = ApiKeyService.GenerateJwt(finalApiKey)
};
return response;
}
[HttpPatch("{id}")]
[Authorize(Policy = "permissions:admin.apikeys.update")]
public async Task<ApiKeyResponse> Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request)
{
var apiKey = await ApiKeyRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404);
apiKey.Description = request.Description;
await ApiKeyRepository.Update(apiKey);
return new ApiKeyResponse()
{
Id = apiKey.Id,
Description = apiKey.Description,
Permissions = apiKey.Permissions,
ExpiresAt = apiKey.ExpiresAt
};
}
[HttpDelete("{id}")]
[Authorize(Policy = "permissions:admin.apikeys.delete")]
public async Task Delete([FromRoute] int id)
{
var apiKey = await ApiKeyRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404);
await ApiKeyRepository.Remove(apiKey);
}
}

View File

@@ -1,27 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
[Authorize]
[ApiController]
[Route("api/admin/system/advanced")]
public class AdvancedController : Controller
{
private readonly FrontendService FrontendService;
public AdvancedController(FrontendService frontendService)
{
FrontendService = frontendService;
}
[HttpGet("frontend")]
[Authorize(Policy = "permissions:admin.system.advanced.frontend")]
public async Task Frontend()
{
var stream = await FrontendService.GenerateZip();
await Results.File(stream, fileDownloadName: "frontend.zip").ExecuteAsync(HttpContext);
}
}

View File

@@ -1,128 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Mappers;
using Moonlight.Shared.Http.Requests.Admin.Sys.Theme;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Customisation;
[ApiController]
[Route("api/admin/system/customisation/themes")]
public class ThemesController : Controller
{
private readonly DatabaseRepository<Theme> ThemeRepository;
public ThemesController(DatabaseRepository<Theme> themeRepository)
{
ThemeRepository = themeRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
public async Task<PagedData<ThemeResponse>> Get(
[FromQuery] [Range(0, int.MaxValue)] int page,
[FromQuery] [Range(1, 100)] int pageSize
)
{
var count = await ThemeRepository.Get().CountAsync();
var items = await ThemeRepository
.Get()
.Skip(page * pageSize)
.Take(pageSize)
.ToArrayAsync();
var mappedItems = items
.Select(ThemeMapper.ToResponse)
.ToArray();
return new PagedData<ThemeResponse>()
{
CurrentPage = page,
Items = mappedItems,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
public async Task<ThemeResponse> GetSingle([FromRoute] int id)
{
var theme = await ThemeRepository
.Get()
.FirstOrDefaultAsync(t => t.Id == id);
if (theme == null)
throw new HttpApiException("Theme with this id not found", 404);
return ThemeMapper.ToResponse(theme);
}
[HttpPost]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task<ThemeResponse> Create([FromBody] CreateThemeRequest request)
{
var theme = ThemeMapper.ToTheme(request);
var finalTheme = await ThemeRepository.Add(theme);
return ThemeMapper.ToResponse(finalTheme);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task<ThemeResponse> Update([FromRoute] int id, [FromBody] UpdateThemeRequest request)
{
var theme = await ThemeRepository
.Get()
.FirstOrDefaultAsync(t => t.Id == id);
if (theme == null)
throw new HttpApiException("Theme with this id not found", 404);
// Disable all other enabled themes if we are enabling the current theme.
// This ensures only one theme is enabled at the time
if (request.IsEnabled)
{
var otherThemes = await ThemeRepository
.Get()
.Where(x => x.IsEnabled && x.Id != id)
.ToArrayAsync();
foreach (var otherTheme in otherThemes)
otherTheme.IsEnabled = false;
await ThemeRepository.RunTransaction(set =>
{
set.UpdateRange(otherThemes);
});
}
ThemeMapper.Merge(theme, request);
await ThemeRepository.Update(theme);
return ThemeMapper.ToResponse(theme);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task Delete([FromRoute] int id)
{
var theme = await ThemeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (theme == null)
throw new HttpApiException("Theme with this id not found", 404);
await ThemeRepository.Remove(theme);
}
}

View File

@@ -1,41 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Requests.Admin.Sys;
using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
[ApiController]
[Route("api/admin/system/diagnose")]
[Authorize(Policy = "permissions:admin.system.diagnose")]
public class DiagnoseController : Controller
{
private readonly DiagnoseService DiagnoseService;
public DiagnoseController(DiagnoseService diagnoseService)
{
DiagnoseService = diagnoseService;
}
[HttpPost]
public async Task Diagnose([FromBody] GenerateDiagnoseRequest request)
{
var stream = await DiagnoseService.GenerateDiagnose(request.Providers);
await Results.Stream(
stream,
contentType: "application/zip",
fileDownloadName: "diagnose.zip"
)
.ExecuteAsync(HttpContext);
}
[HttpGet("providers")]
public async Task<DiagnoseProvideResponse[]> GetProviders()
{
return await DiagnoseService.GetProviders();
}
}

View File

@@ -1,475 +0,0 @@
using System.Text;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
[ApiController]
[Route("api/admin/system/files")]
[Authorize(Policy = "permissions:admin.system.files")]
public class FilesController : Controller
{
private readonly string BaseDirectory = "storage";
private readonly long MaxChunkSize = ByteConverter.FromMegaBytes(20).Bytes;
[HttpPost("touch")]
public async Task CreateFile([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
if (System.IO.File.Exists(physicalPath))
throw new HttpApiException("A file already exists at that path", 400);
if (Directory.Exists(path))
throw new HttpApiException("A folder already exists at that path", 400);
await using var fs = System.IO.File.Create(physicalPath);
fs.Close();
}
[HttpPost("mkdir")]
public Task CreateFolder([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
if (Directory.Exists(path))
throw new HttpApiException("A folder already exists at that path", 400);
if (System.IO.File.Exists(physicalPath))
throw new HttpApiException("A file already exists at that path", 400);
Directory.CreateDirectory(physicalPath);
return Task.CompletedTask;
}
[HttpGet("list")]
public Task<FileSystemEntryResponse[]> List([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
var entries = new List<FileSystemEntryResponse>();
var files = Directory.GetFiles(physicalPath);
foreach (var file in files)
{
var fi = new FileInfo(file);
entries.Add(new FileSystemEntryResponse()
{
Name = fi.Name,
Size = fi.Length,
CreatedAt = fi.CreationTimeUtc,
IsFolder = false,
UpdatedAt = fi.LastWriteTimeUtc
});
}
var directories = Directory.GetDirectories(physicalPath);
foreach (var directory in directories)
{
var di = new DirectoryInfo(directory);
entries.Add(new FileSystemEntryResponse()
{
Name = di.Name,
Size = 0,
CreatedAt = di.CreationTimeUtc,
UpdatedAt = di.LastWriteTimeUtc,
IsFolder = true
});
}
return Task.FromResult(
entries.ToArray()
);
}
[HttpPost("upload")]
public async Task Upload([FromQuery] string path, [FromQuery] long chunkSize, [FromQuery] long totalSize, [FromQuery] int chunkId)
{
if (Request.Form.Files.Count != 1)
throw new HttpApiException("You need to provide exactly one file", 400);
var file = Request.Form.Files[0];
if (file.Length > chunkSize)
throw new HttpApiException("The provided data exceeds the chunk size limit", 400);
var chunks = totalSize / chunkSize;
chunks += totalSize % chunkSize > 0 ? 1 : 0;
if (chunkId > chunks)
throw new HttpApiException("Invalid chunk id: Out of bounds", 400);
var positionToSkipTo = chunkSize * chunkId;
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
var baseDir = Path.GetDirectoryName(physicalPath);
if (!string.IsNullOrEmpty(baseDir))
Directory.CreateDirectory(baseDir);
await using var fs = System.IO.File.Open(physicalPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
// This resizes the file to the correct size so we can handle the chunk if it didnt exist
if (fs.Length != totalSize)
fs.SetLength(totalSize);
fs.Position = positionToSkipTo;
var dataStream = file.OpenReadStream();
await dataStream.CopyToAsync(fs);
await fs.FlushAsync();
fs.Close();
}
[HttpPost("move")]
public Task Move([FromQuery] string oldPath, [FromQuery] string newPath)
{
var oldSafePath = SanitizePath(oldPath);
var newSafePath = SanitizePath(newPath);
var oldPhysicalDirPath = Path.Combine(BaseDirectory, oldSafePath);
if (Directory.Exists(oldPhysicalDirPath))
{
var newPhysicalDirPath = Path.Combine(BaseDirectory, newSafePath);
Directory.Move(
oldPhysicalDirPath,
newPhysicalDirPath
);
}
else
{
var oldPhysicalFilePath = Path.Combine(BaseDirectory, oldSafePath);
var newPhysicalFilePath = Path.Combine(BaseDirectory, newSafePath);
System.IO.File.Move(
oldPhysicalFilePath,
newPhysicalFilePath
);
}
return Task.CompletedTask;
}
[HttpDelete("delete")]
public Task Delete([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalDirPath = Path.Combine(BaseDirectory, safePath);
if (Directory.Exists(physicalDirPath))
Directory.Delete(physicalDirPath, true);
else
{
var physicalFilePath = Path.Combine(BaseDirectory, safePath);
System.IO.File.Delete(physicalFilePath);
}
return Task.CompletedTask;
}
[HttpGet("download")]
public async Task Download([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath);
await using var fs = System.IO.File.Open(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await fs.CopyToAsync(Response.Body);
fs.Close();
}
[HttpPost("compress")]
public async Task Compress([FromBody] CompressRequest request)
{
if (request.Type == "tar.gz")
await CompressTarGz(request.Path, request.ItemsToCompress);
else if (request.Type == "zip")
await CompressZip(request.Path, request.ItemsToCompress);
}
#region Tar Gz
private async Task CompressTarGz(string path, string[] itemsToCompress)
{
var safePath = SanitizePath(path);
var destination = Path.Combine(BaseDirectory, safePath);
await using var outStream = System.IO.File.Create(destination);
await using var gzoStream = new GZipOutputStream(outStream);
await using var tarStream = new TarOutputStream(gzoStream, Encoding.UTF8);
foreach (var itemName in itemsToCompress)
{
var safeFilePath = SanitizePath(itemName);
var filePath = Path.Combine(BaseDirectory, safeFilePath);
var fi = new FileInfo(filePath);
if (fi.Exists)
await AddFileToTarGz(tarStream, filePath);
else
{
var safeDirePath = SanitizePath(itemName);
var dirPath = Path.Combine(BaseDirectory, safeDirePath);
await AddDirectoryToTarGz(tarStream, dirPath);
}
}
await tarStream.FlushAsync();
await gzoStream.FlushAsync();
await outStream.FlushAsync();
tarStream.Close();
gzoStream.Close();
outStream.Close();
}
private async Task AddDirectoryToTarGz(TarOutputStream tarOutputStream, string root)
{
foreach (var file in Directory.GetFiles(root))
await AddFileToTarGz(tarOutputStream, file);
foreach (var directory in Directory.GetDirectories(root))
await AddDirectoryToTarGz(tarOutputStream, directory);
}
private async Task AddFileToTarGz(TarOutputStream tarOutputStream, string file)
{
// Open file stream
var fs = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Meta
var entry = TarEntry.CreateTarEntry(file);
// Fix path
entry.Name = Formatter
.ReplaceStart(entry.Name, BaseDirectory, "")
.TrimStart('/');
entry.Size = fs.Length;
// Write entry
await tarOutputStream.PutNextEntryAsync(entry, CancellationToken.None);
// Copy file content to tar stream
await fs.CopyToAsync(tarOutputStream);
fs.Close();
// Close the entry
tarOutputStream.CloseEntry();
}
#endregion
#region ZIP
private async Task CompressZip(string path, string[] itemsToCompress)
{
var safePath = SanitizePath(path);
var destination = Path.Combine(BaseDirectory, safePath);
await using var outStream = System.IO.File.Create(destination);
await using var zipOutputStream = new ZipOutputStream(outStream);
foreach (var itemName in itemsToCompress)
{
var safeFilePath = SanitizePath(itemName);
var filePath = Path.Combine(BaseDirectory, safeFilePath);
var fi = new FileInfo(filePath);
if (fi.Exists)
await AddFileToZip(zipOutputStream, filePath);
else
{
var safeDirePath = SanitizePath(itemName);
var dirPath = Path.Combine(BaseDirectory, safeDirePath);
await AddDirectoryToZip(zipOutputStream, dirPath);
}
}
await zipOutputStream.FlushAsync();
await outStream.FlushAsync();
zipOutputStream.Close();
outStream.Close();
}
private async Task AddFileToZip(ZipOutputStream zipOutputStream, string path)
{
// Open file stream
var fs = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Fix path
var name = Formatter
.ReplaceStart(path, BaseDirectory, "")
.TrimStart('/');
// Meta
var entry = new ZipEntry(name);
entry.Size = fs.Length;
// Write entry
await zipOutputStream.PutNextEntryAsync(entry, CancellationToken.None);
// Copy file content to tar stream
await fs.CopyToAsync(zipOutputStream);
fs.Close();
// Close the entry
zipOutputStream.CloseEntry();
}
private async Task AddDirectoryToZip(ZipOutputStream zipOutputStream, string root)
{
foreach (var file in Directory.GetFiles(root))
await AddFileToZip(zipOutputStream, file);
foreach (var directory in Directory.GetDirectories(root))
await AddDirectoryToZip(zipOutputStream, directory);
}
#endregion
[HttpPost("decompress")]
public async Task Decompress([FromBody] DecompressRequest request)
{
if (request.Type == "tar.gz")
await DecompressTarGz(request.Path, request.Destination);
else if (request.Type == "zip")
await DecompressZip(request.Path, request.Destination);
}
#region Tar Gz
private async Task DecompressTarGz(string path, string destination)
{
var safeDestination = SanitizePath(destination);
var safeArchivePath = SanitizePath(path);
var archivePath = Path.Combine(BaseDirectory, safeArchivePath);
await using var fs = System.IO.File.Open(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await using var gzipInputStream = new GZipInputStream(fs);
await using var tarInputStream = new TarInputStream(gzipInputStream, Encoding.UTF8);
while (true)
{
var entry = await tarInputStream.GetNextEntryAsync(CancellationToken.None);
if (entry == null)
break;
var safeFilePath = SanitizePath(entry.Name);
var fileDestination = Path.Combine(BaseDirectory, safeDestination, safeFilePath);
var parentFolder = Path.GetDirectoryName(fileDestination);
// Ensure parent directory exists, if it's not the base directory
if (parentFolder != null && parentFolder != BaseDirectory)
Directory.CreateDirectory(parentFolder);
await using var fileDestinationFs =
System.IO.File.Open(fileDestination, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
await tarInputStream.CopyToAsync(fileDestinationFs, CancellationToken.None);
await fileDestinationFs.FlushAsync();
fileDestinationFs.Close();
}
tarInputStream.Close();
gzipInputStream.Close();
fs.Close();
}
#endregion
#region Zip
private async Task DecompressZip(string path, string destination)
{
var safeDestination = SanitizePath(destination);
var safeArchivePath = SanitizePath(path);
var archivePath = Path.Combine(BaseDirectory, safeArchivePath);
await using var fs = System.IO.File.Open(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await using var zipInputStream = new ZipInputStream(fs);
while (true)
{
var entry = zipInputStream.GetNextEntry();
if (entry == null)
break;
if (entry.IsDirectory)
continue;
var safeFilePath = SanitizePath(entry.Name);
var fileDestination = Path.Combine(BaseDirectory, safeDestination, safeFilePath);
var parentFolder = Path.GetDirectoryName(fileDestination);
// Ensure parent directory exists, if it's not the base directory
if (parentFolder != null && parentFolder != BaseDirectory)
Directory.CreateDirectory(parentFolder);
await using var fileDestinationFs =
System.IO.File.Open(fileDestination, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
await zipInputStream.CopyToAsync(fileDestinationFs, CancellationToken.None);
await fileDestinationFs.FlushAsync();
fileDestinationFs.Close();
}
zipInputStream.Close();
fs.Close();
}
#endregion
private string SanitizePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
// Normalize separators
path = path.Replace('\\', '/');
// Remove ".." and "."
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Where(part => part != ".." && part != ".");
var sanitized = string.Join("/", parts);
// Ensure it does not start with a slash
if (sanitized.StartsWith('/'))
sanitized = sanitized.TrimStart('/');
return sanitized;
}
}

View File

@@ -1,38 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
[ApiController]
[Route("api/admin/system")]
public class SystemController : Controller
{
private readonly ApplicationService ApplicationService;
public SystemController(ApplicationService applicationService)
{
ApplicationService = applicationService;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.system.overview")]
public async Task<SystemOverviewResponse> GetOverview()
{
return new()
{
Uptime = await ApplicationService.GetUptime(),
CpuUsage = await ApplicationService.GetCpuUsage(),
MemoryUsage = await ApplicationService.GetMemoryUsage(),
OperatingSystem = await ApplicationService.GetOsName()
};
}
[HttpPost("shutdown")]
[Authorize(Policy = "permissions:admin.system.shutdown")]
public async Task Shutdown()
{
await ApplicationService.Shutdown();
}
}

View File

@@ -1,192 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.ApiServer.Http.Controllers.Admin.Users;
[ApiController]
[Route("api/admin/users")]
public class UsersController : Controller
{
private readonly DatabaseRepository<User> UserRepository;
public UsersController(DatabaseRepository<User> userRepository)
{
UserRepository = userRepository;
}
[HttpGet]
[Authorize(Policy = "permissions:admin.users.get")]
public async Task<IPagedData<UserResponse>> Get(
[FromQuery] [Range(0, int.MaxValue)] int page,
[FromQuery] [Range(1, 100)] int pageSize
)
{
var count = await UserRepository.Get().CountAsync();
var users = await UserRepository
.Get()
.OrderBy(x => x.Id)
.Skip(page * pageSize)
.Take(pageSize)
.ToArrayAsync();
var mappedUsers = users
.Select(x => new UserResponse()
{
Id = x.Id,
Email = x.Email,
Username = x.Username,
Permissions = x.Permissions
})
.ToArray();
return new PagedData<UserResponse>()
{
CurrentPage = page,
Items = mappedUsers,
PageSize = pageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
};
}
[HttpGet("{id}")]
[Authorize(Policy = "permissions:admin.users.get")]
public async Task<UserResponse> GetSingle(int id)
{
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
throw new HttpApiException("No user with that id found", 404);
return new UserResponse()
{
Id = user.Id,
Email = user.Email,
Username = user.Username,
Permissions = user.Permissions
};
}
[HttpPost]
[Authorize(Policy = "permissions:admin.users.create")]
public async Task<UserResponse> Create([FromBody] CreateUserRequest request)
{
// Reformat values
request.Username = request.Username.ToLower().Trim();
request.Email = request.Email.ToLower().Trim();
// Check for users with the same values
if (UserRepository.Get().Any(x => x.Username == request.Username))
throw new HttpApiException("A user with that username already exists", 400);
if (UserRepository.Get().Any(x => x.Email == request.Email))
throw new HttpApiException("A user with that email address already exists", 400);
var hashedPassword = HashHelper.Hash(request.Password);
var user = new User()
{
Email = request.Email,
Username = request.Username,
Password = hashedPassword,
Permissions = request.Permissions
};
var finalUser = await UserRepository.Add(user);
return new UserResponse()
{
Id = finalUser.Id,
Email = finalUser.Email,
Username = finalUser.Username,
Permissions = finalUser.Permissions
};
}
[HttpPatch("{id}")]
[Authorize(Policy = "permissions:admin.users.update")]
public async Task<UserResponse> Update([FromRoute] int id, [FromBody] UpdateUserRequest request)
{
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
throw new HttpApiException("No user with that id found", 404);
// Reformat values
request.Username = request.Username.ToLower().Trim();
request.Email = request.Email.ToLower().Trim();
// Check for users with the same values
if (UserRepository.Get().Any(x => x.Username == request.Username && x.Id != user.Id))
throw new HttpApiException("A user with that username already exists", 400);
if (UserRepository.Get().Any(x => x.Email == request.Email && x.Id != user.Id))
throw new HttpApiException("A user with that email address already exists", 400);
// Perform hashing the password if required
if (!string.IsNullOrEmpty(request.Password))
{
user.Password = HashHelper.Hash(request.Password);
user.TokenValidTimestamp = DateTime.UtcNow; // Log out user after password change
}
if (request.Permissions.Any(x => !user.Permissions.Contains(x)))
{
user.Permissions = request.Permissions;
user.TokenValidTimestamp = DateTime.UtcNow; // Log out user after permission change
}
user.Email = request.Email;
user.Username = request.Username;
await UserRepository.Update(user);
return new UserResponse()
{
Id = user.Id,
Email = user.Email,
Username = user.Username,
Permissions = user.Permissions
};
}
[HttpDelete("{id}")]
[Authorize(Policy = "permissions:admin.users.delete")]
public async Task Delete([FromRoute] int id, [FromQuery] bool force = false)
{
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
throw new HttpApiException("No user with that id found", 404);
var deletionService = HttpContext.RequestServices.GetRequiredService<UserDeletionService>();
if (!force)
{
var validationResult = await deletionService.Validate(user);
if (!validationResult.IsAllowed)
throw new HttpApiException($"Unable to delete user", 400, validationResult.Reason);
}
await deletionService.Delete(user, force);
}
}

View File

@@ -1,111 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Interfaces;
using Moonlight.Shared.Http.Requests.Auth;
using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.ApiServer.Http.Controllers.Auth;
[ApiController]
[Route("api/auth")]
public class AuthController : Controller
{
private readonly AppConfiguration Configuration;
private readonly DatabaseRepository<User> UserRepository;
private readonly IOAuth2Provider OAuth2Provider;
public AuthController(
AppConfiguration configuration,
DatabaseRepository<User> userRepository,
IOAuth2Provider oAuth2Provider
)
{
UserRepository = userRepository;
OAuth2Provider = oAuth2Provider;
Configuration = configuration;
}
[AllowAnonymous]
[HttpGet("start")]
public async Task<LoginStartResponse> Start()
{
var url = await OAuth2Provider.Start();
return new LoginStartResponse()
{
Url = url
};
}
[AllowAnonymous]
[HttpPost("complete")]
public async Task<LoginCompleteResponse> Complete([FromBody] LoginCompleteRequest request)
{
var user = await OAuth2Provider.Complete(request.Code);
if (user == null)
throw new HttpApiException("Unable to load user data", 500);
// Generate token
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
Expires = DateTime.Now.AddHours(Configuration.Authentication.TokenDuration),
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now.AddMinutes(-1),
Claims = new Dictionary<string, object>()
{
{
"userId",
user.Id
},
{
"permissions",
string.Join(";", user.Permissions)
}
},
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration.Authentication.Secret)
),
SecurityAlgorithms.HmacSha256
),
Issuer = Configuration.PublicUrl,
Audience = Configuration.PublicUrl
};
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
var jwt = jwtSecurityTokenHandler.WriteToken(securityToken);
return new()
{
AccessToken = jwt
};
}
[Authorize]
[HttpGet("check")]
public async Task<CheckResponse> Check()
{
var userIdStr = User.FindFirstValue("userId")!;
var userId = int.Parse(userIdStr);
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
return new()
{
Email = user.Email,
Username = user.Username,
Permissions = user.Permissions
};
}
}

View File

@@ -1,31 +0,0 @@
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Misc;
namespace Moonlight.ApiServer.Http.Controllers.Frontend;
[ApiController]
[Route("/")]
public class FrontendController : Controller
{
private readonly FrontendService FrontendService;
public FrontendController(FrontendService frontendService)
{
FrontendService = frontendService;
}
[HttpGet("frontend.json")]
public async Task<FrontendConfiguration> GetConfiguration()
=> await FrontendService.GetConfiguration();
[HttpGet]
public async Task<IResult> Index()
{
var content = await FrontendService.GenerateIndexHtml();
return Results.Text(content, "text/html", Encoding.UTF8);
}
}

View File

@@ -1,106 +0,0 @@
@using Moonlight.ApiServer.Database.Entities
@using Moonlight.Shared.Misc
<!DOCTYPE html>
<html lang="en" class="bg-background text-base-content font-inter">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>@Title</title>
<base href="/"/>
@foreach (var style in Styles)
{
<link rel="stylesheet" href="@style"/>
}
<link href="manifest.webmanifest" rel="manifest"/>
<link rel="apple-touch-icon" sizes="512x512" href="/_content/Moonlight.Client/img/icon-512.png"/>
<link rel="apple-touch-icon" sizes="192x192" href="/_content/Moonlight.Client/img/icon-192.png"/>
@if (Theme != null)
{
<style>
:root {
--mooncore-color-background: @(Theme.Content.ColorBackground);
--mooncore-color-base-100: @(Theme.Content.ColorBase100);
--mooncore-color-base-150: @(Theme.Content.ColorBase150);
--mooncore-color-base-200: @(Theme.Content.ColorBase200);
--mooncore-color-base-250: @(Theme.Content.ColorBase250);
--mooncore-color-base-300: @(Theme.Content.ColorBase300);
--mooncore-color-base-content: @(Theme.Content.ColorBaseContent);
--mooncore-color-primary: @(Theme.Content.ColorPrimary);
--mooncore-color-primary-content: @(Theme.Content.ColorPrimaryContent);
--mooncore-color-secondary: @(Theme.Content.ColorSecondary);
--mooncore-color-secondary-content: @(Theme.Content.ColorSecondaryContent);
--mooncore-color-accent: @(Theme.Content.ColorAccent);
--mooncore-color-accent-content: @(Theme.Content.ColorAccentContent);
--mooncore-color-neutral: @(Theme.Content.ColorNeutral);
--mooncore-color-neutral-content: @(Theme.Content.ColorNeutralContent);
--mooncore-color-info: @(Theme.Content.ColorInfo);
--mooncore-color-info-content: @(Theme.Content.ColorInfoContent);
--mooncore-color-success: @(Theme.Content.ColorSuccess);
--mooncore-color-success-content: @(Theme.Content.ColorSuccessContent);
--mooncore-color-warning: @(Theme.Content.ColorWarning);
--mooncore-color-warning-content: @(Theme.Content.ColorWarningContent);
--mooncore-color-error: @(Theme.Content.ColorError);
--mooncore-color-error-content: @(Theme.Content.ColorErrorContent);
--mooncore-radius-selector: @(Theme.Content.RadiusSelector)rem;
--mooncore-radius-field: @(Theme.Content.RadiusField)rem;
--mooncore-radius-box: @(Theme.Content.RadiusBox)rem;
--mooncore-size-selector: @(Theme.Content.SizeSelector)rem;
--mooncore-size-field: @(Theme.Content.SizeField)rem;
--mooncore-border: @(Theme.Content.Border)px;
--mooncore-depth: @(Theme.Content.Depth);
--mooncore-noise: @(Theme.Content.Noise);
}
</style>
}
</head>
<body>
<div id="app">
<div class="flex h-screen justify-center items-center">
<div class="sm:max-w-lg">
<div id="blazor-loader-label" class="text-center mb-2 text-lg font-semibold"></div>
<div class="flex flex-col gap-1">
<div class="progress h-3 min-w-sm md:min-w-md" role="progressbar" aria-valuemin="0" aria-valuemax="100">
<div id="blazor-loader-progress" class="progress-bar progress-primary"></div>
</div>
</div>
</div>
</div>
</div>
@foreach (var script in Scripts)
{
<script src="@script"></script>
}
<script src="/_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>
</html>
@code
{
[Parameter] public string Title { get; set; }
[Parameter] public string[] Scripts { get; set; }
[Parameter] public string[] Styles { get; set; }
[Parameter] public Theme? Theme { get; set; }
}

View File

@@ -1,62 +0,0 @@
<html lang="en" class="h-full bg-background">
<head>
<title>Login into your account</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" type="text/css" href="/css/style.min.css"/>
</head>
<body class="h-full">
<div class="flex h-auto min-h-screen items-center justify-center overflow-x-hidden py-10">
<div class="relative flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div
class="bg-base-100 shadow-base-300/20 z-1 w-full space-y-6 rounded-xl p-6 shadow-md sm:min-w-md lg:p-8">
<div class="flex justify-center items-center gap-3">
<img src="/_content/Moonlight.Client/svg/logo.svg" class="size-12" alt="brand-logo"/>
</div>
<div class="text-center">
<h3 class="text-base-content mb-1.5 text-2xl font-semibold">Login into your account</h3>
<p class="text-base-content/80">After logging in you will be able to manage your services</p>
</div>
<div class="space-y-4">
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div class="alert alert-error text-center">
@ErrorMessage
</div>
}
<form class="mb-4 space-y-4" method="post">
<div>
<label class="label-text" for="email">Email address</label>
<input type="email" name="email" placeholder="Enter your email address" class="input" id="email"
required/>
</div>
<div>
<label class="label-text" for="password">Password</label>
<input class="input" name="password" id="password" type="password" placeholder="············"
required/>
</div>
<button class="btn btn-lg btn-primary btn-gradient btn-block">Login</button>
</form>
<p class="text-base-content/80 mb-4 text-center">
No account?
<a href="?client_id=@(ClientId)&redirect_uri=@(RedirectUri)&response_type=@(ResponseType)&view=register"
class="link link-animated link-primary font-normal">Create an account</a>
</p>
</div>
</div>
</div>
</div>
</body>
</html>
@code
{
[Parameter] public string ClientId { get; set; }
[Parameter] public string RedirectUri { get; set; }
[Parameter] public string ResponseType { get; set; }
[Parameter] public string? ErrorMessage { get; set; }
}

View File

@@ -1,317 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Helpers;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.Shared.Http.Responses.OAuth2;
namespace Moonlight.ApiServer.Http.Controllers.OAuth2;
[ApiController]
[Route("oauth2")]
public partial class OAuth2Controller : Controller
{
private readonly AppConfiguration Configuration;
private readonly DatabaseRepository<User> UserRepository;
private readonly string ExpectedRedirectUri;
public OAuth2Controller(AppConfiguration configuration, DatabaseRepository<User> userRepository)
{
Configuration = configuration;
UserRepository = userRepository;
ExpectedRedirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect)
? Configuration.PublicUrl
: Configuration.Authentication.OAuth2.AuthorizationRedirect;
}
[AllowAnonymous]
[HttpGet("authorize")]
public async Task Authorize(
[FromQuery(Name = "client_id")] string clientId,
[FromQuery(Name = "redirect_uri")] string redirectUri,
[FromQuery(Name = "response_type")] string responseType,
[FromQuery(Name = "view")] string view = "login"
)
{
if (!Configuration.Authentication.EnableLocalOAuth2)
throw new HttpApiException("Local OAuth2 has been disabled", 403);
if (Configuration.Authentication.OAuth2.ClientId != clientId ||
redirectUri != ExpectedRedirectUri ||
responseType != "code")
{
throw new HttpApiException("Invalid oauth2 request", 400);
}
string html;
if (view == "register")
{
html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
parameters.Add("ResponseType", responseType);
});
}
else
{
html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
parameters.Add("ResponseType", responseType);
});
}
await Results
.Text(html, "text/html", Encoding.UTF8)
.ExecuteAsync(HttpContext);
}
[AllowAnonymous]
[HttpPost("authorize")]
public async Task AuthorizePost(
[FromQuery(Name = "client_id")] string clientId,
[FromQuery(Name = "redirect_uri")] string redirectUri,
[FromQuery(Name = "response_type")] string responseType,
[FromForm(Name = "email")] [EmailAddress(ErrorMessage = "You need to provide a valid email address")] string email,
[FromForm(Name = "password")] string password,
[FromForm(Name = "username")] string username = "",
[FromQuery(Name = "view")] string view = "login"
)
{
if (!Configuration.Authentication.EnableLocalOAuth2)
throw new HttpApiException("Local OAuth2 has been disabled", 403);
if (Configuration.Authentication.OAuth2.ClientId != clientId ||
redirectUri != ExpectedRedirectUri ||
responseType != "code")
{
throw new HttpApiException("Invalid oauth2 request", 400);
}
if (view == "register" && string.IsNullOrEmpty(username))
throw new HttpApiException("You need to provide a username", 400);
string? errorMessage = null;
try
{
if (view == "register")
{
var user = await Register(username, email, password);
var code = await GenerateCode(user);
Response.Redirect($"{redirectUri}?code={code}");
}
else
{
var user = await Login(email, password);
var code = await GenerateCode(user);
Response.Redirect($"{redirectUri}?code={code}");
}
}
catch (HttpApiException e)
{
errorMessage = e.Title;
string html;
if (view == "register")
{
html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
parameters.Add("ResponseType", responseType);
parameters.Add("ErrorMessage", errorMessage!);
});
}
else
{
html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
parameters.Add("ResponseType", responseType);
parameters.Add("ErrorMessage", errorMessage!);
});
}
await Results
.Text(html, "text/html", Encoding.UTF8)
.ExecuteAsync(HttpContext);
}
}
[AllowAnonymous]
[HttpPost("handle")]
public async Task<OAuth2HandleResponse> Handle(
[FromForm(Name = "grant_type")] string grantType,
[FromForm(Name = "code")] string code,
[FromForm(Name = "redirect_uri")] string redirectUri,
[FromForm(Name = "client_id")] string clientId
)
{
if (!Configuration.Authentication.EnableLocalOAuth2)
throw new HttpApiException("Local OAuth2 has been disabled", 403);
// Check header
if (!Request.Headers.ContainsKey("Authorization"))
throw new HttpApiException("You are missing the Authorization header", 400);
var authorizationHeaderValue = Request.Headers["Authorization"].FirstOrDefault() ?? "";
if (authorizationHeaderValue != $"Basic {Configuration.Authentication.OAuth2.ClientSecret}")
throw new HttpApiException("Invalid Authorization header value", 400);
// Check form
if (grantType != "authorization_code")
throw new HttpApiException("Invalid grant type provided", 400);
if (clientId != Configuration.Authentication.OAuth2.ClientId)
throw new HttpApiException("Invalid client id provided", 400);
if (redirectUri != ExpectedRedirectUri)
throw new HttpApiException("Invalid redirect uri provided", 400);
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
ClaimsPrincipal? codeData;
try
{
codeData = jwtSecurityTokenHandler.ValidateToken(code, new TokenValidationParameters()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
Configuration.Authentication.OAuth2.Secret
)),
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidateAudience = false,
ValidateIssuer = false
}, out _);
}
catch (SecurityTokenException)
{
throw new HttpApiException("Invalid code provided", 400);
}
if (codeData == null)
throw new HttpApiException("Invalid code provided", 400);
var userIdClaim = codeData.Claims.FirstOrDefault(x => x.Type == "id");
if (userIdClaim == null)
throw new HttpApiException("Malformed code provided", 400);
if (!int.TryParse(userIdClaim.Value, out var userId))
throw new HttpApiException("Malformed code provided", 400);
var user = UserRepository
.Get()
.FirstOrDefault(x => x.Id == userId);
if (user == null)
throw new HttpApiException("Malformed code provided", 400);
return new()
{
UserId = user.Id
};
}
private Task<string> GenerateCode(User user)
{
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
Expires = DateTime.Now.AddMinutes(1),
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now.AddMinutes(-1),
Claims = new Dictionary<string, object>()
{
{
"id",
user.Id
}
},
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration.Authentication.OAuth2.Secret)
),
SecurityAlgorithms.HmacSha256
)
};
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
return Task.FromResult(
jwtSecurityTokenHandler.WriteToken(securityToken)
);
}
private async Task<User> Register(string username, string email, string password)
{
if (await UserRepository.Get().AnyAsync(x => x.Username == username))
throw new HttpApiException("A account with that username already exists", 400);
if (await UserRepository.Get().AnyAsync(x => x.Email == email))
throw new HttpApiException("A account with that email already exists", 400);
if (!UsernameRegex().IsMatch(username))
throw new HttpApiException("The username is only allowed to be contained out of small characters and numbers", 400);
var user = new User()
{
Username = username,
Email = email,
Password = HashHelper.Hash(password),
};
if (Configuration.Authentication.OAuth2.FirstUserAdmin)
{
var userCount = await UserRepository.Get().CountAsync();
if (userCount == 0)
user.Permissions = ["*"];
}
return await UserRepository.Add(user);
}
private async Task<User> Login(string email, string password)
{
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Email == email);
if (user == null)
throw new HttpApiException("Invalid combination of email and password", 400);
if (!HashHelper.Verify(password, user.Password))
throw new HttpApiException("Invalid combination of email and password", 400);
return user;
}
[GeneratedRegex("^[a-z][a-z0-9]*$")]
private static partial Regex UsernameRegex();
}

View File

@@ -1,66 +0,0 @@
<html lang="en" class="h-full bg-background">
<head>
<title>Register a new account</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" type="text/css" href="/css/style.min.css"/>
</head>
<body class="h-full">
<div class="flex h-auto min-h-screen items-center justify-center overflow-x-hidden py-10">
<div class="relative flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div
class="bg-base-100 shadow-base-300/20 z-1 w-full space-y-6 rounded-xl p-6 shadow-md sm:min-w-md lg:p-8">
<div class="flex justify-center items-center gap-3">
<img src="/_content/Moonlight.Client/svg/logo.svg" class="size-12" alt="brand-logo"/>
</div>
<div class="text-center">
<h3 class="text-base-content mb-1.5 text-2xl font-semibold">Register a new account</h3>
<p class="text-base-content/80">After signing up you will be able to manage your services</p>
</div>
<div class="space-y-4">
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div class="alert alert-error text-center">
@ErrorMessage
</div>
}
<form class="mb-4 space-y-4" method="post">
<div>
<label class="label-text" for="username">Username</label>
<input type="text" name="username" placeholder="Enter your username" class="input" id="username"
required/>
</div>
<div>
<label class="label-text" for="email">Email address</label>
<input type="email" name="email" placeholder="Enter your email address" class="input" id="email"
required/>
</div>
<div>
<label class="label-text" for="password">Password</label>
<input class="input" name="password" id="password" type="password" placeholder="············"
required/>
</div>
<button class="btn btn-lg btn-primary btn-gradient btn-block">Register</button>
</form>
<p class="text-base-content/80 mb-4 text-center">
Already registered?
<a href="?client_id=@(ClientId)&redirect_uri=@(RedirectUri)&response_type=@(ResponseType)&view=login"
class="link link-animated link-primary font-normal">Login into your account</a>
</p>
</div>
</div>
</div>
</div>
</body>
</html>
@code
{
[Parameter] public string ClientId { get; set; }
[Parameter] public string RedirectUri { get; set; }
[Parameter] public string ResponseType { get; set; }
[Parameter] public string? ErrorMessage { get; set; }
}

View File

@@ -1,45 +0,0 @@
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Helpers;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Models;
namespace Moonlight.ApiServer.Http.Controllers.Swagger;
[Route("api/swagger")]
public class SwaggerController : Controller
{
private readonly AppConfiguration Configuration;
private readonly IServiceProvider ServiceProvider;
public SwaggerController(
AppConfiguration configuration,
IServiceProvider serviceProvider
)
{
Configuration = configuration;
ServiceProvider = serviceProvider;
}
[HttpGet]
[Authorize]
public async Task<ActionResult> Get()
{
if (!Configuration.Development.EnableApiDocs)
return BadRequest("Api docs are disabled");
var options = new ApiDocsOptions();
var optionsJson = JsonSerializer.Serialize(options);
var html = await ComponentHelper.RenderComponent<SwaggerPage>(
ServiceProvider,
parameters =>
{
parameters.Add("Options", optionsJson);
}
);
return Content(html, "text/html");
}
}

View File

@@ -1,92 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Moonlight Api Reference</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<script id="api-reference" data-url="/api/swagger/main"></script>
<script>
const configuration = @(Options)
document.getElementById('api-reference').dataset.configuration = JSON.stringify(configuration)
</script>
<script src="https://cdn.jsdelivr.net/npm/@@scalar/api-reference"></script>
<style>.light-mode {
--scalar-background-1: #fff;
--scalar-background-2: #f8fafc;
--scalar-background-3: #e7e7e7;
--scalar-background-accent: #8ab4f81f;
--scalar-color-1: #000;
--scalar-color-2: #6b7280;
--scalar-color-3: #9ca3af;
--scalar-color-accent: #00c16a;
--scalar-border-color: #e5e7eb;
--scalar-color-green: #069061;
--scalar-color-red: #ef4444;
--scalar-color-yellow: #f59e0b;
--scalar-color-blue: #1d4ed8;
--scalar-color-orange: #fb892c;
--scalar-color-purple: #6d28d9;
--scalar-button-1: #000;
--scalar-button-1-hover: rgba(0, 0, 0, 0.9);
--scalar-button-1-color: #fff;
}
.dark-mode {
--scalar-background-1: #020420;
--scalar-background-2: #121a31;
--scalar-background-3: #1e293b;
--scalar-background-accent: #8ab4f81f;
--scalar-color-1: #fff;
--scalar-color-2: #cbd5e1;
--scalar-color-3: #94a3b8;
--scalar-color-accent: #00dc82;
--scalar-border-color: #1e293b;
--scalar-color-green: #069061;
--scalar-color-red: #f87171;
--scalar-color-yellow: #fde68a;
--scalar-color-blue: #60a5fa;
--scalar-color-orange: #fb892c;
--scalar-color-purple: #ddd6fe;
--scalar-button-1: hsla(0, 0%, 100%, 0.9);
--scalar-button-1-hover: hsla(0, 0%, 100%, 0.8);
--scalar-button-1-color: #000;
}
.dark-mode .t-doc__sidebar,
.light-mode .t-doc__sidebar {
--scalar-sidebar-background-1: var(--scalar-background-1);
--scalar-sidebar-color-1: var(--scalar-color-1);
--scalar-sidebar-color-2: var(--scalar-color-3);
--scalar-sidebar-border-color: var(--scalar-border-color);
--scalar-sidebar-item-hover-background: transparent;
--scalar-sidebar-item-hover-color: var(--scalar-color-1);
--scalar-sidebar-item-active-background: transparent;
--scalar-sidebar-color-active: var(--scalar-color-accent);
--scalar-sidebar-search-background: transparent;
--scalar-sidebar-search-color: var(--scalar-color-3);
--scalar-sidebar-search-border-color: var(--scalar-border-color);
--scalar-sidebar-indent-border: var(--scalar-border-color);
--scalar-sidebar-indent-border-hover: var(--scalar-color-1);
--scalar-sidebar-indent-border-active: var(--scalar-color-accent);
}
.scalar-card .request-card-footer {
--scalar-background-3: var(--scalar-background-2);
--scalar-button-1: #0f172a;
--scalar-button-1-hover: rgba(30, 41, 59, 0.5);
--scalar-button-1-color: #fff;
}
.scalar-card .show-api-client-button {
border: 1px solid #334155 !important;
}</style>
</body>
</html>
@code
{
[Parameter] public string Options { get; set; }
}

View File

@@ -1,57 +0,0 @@
using System.IO.Compression;
using System.Text.Json;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Extensions;
using Moonlight.ApiServer.Interfaces;
namespace Moonlight.ApiServer.Implementations.Diagnose;
public class CoreConfigDiagnoseProvider : IDiagnoseProvider
{
private readonly AppConfiguration Config;
public CoreConfigDiagnoseProvider(AppConfiguration config)
{
Config = config;
}
private string CheckForNullOrEmpty(string? content)
{
return string.IsNullOrEmpty(content)
? "ISEMPTY"
: "ISNOTEMPTY";
}
public async Task ModifyZipArchive(ZipArchive archive)
{
var json = JsonSerializer.Serialize(Config);
var config = JsonSerializer.Deserialize<AppConfiguration>(json);
if (config == null)
{
await archive.AddText("core/config.txt", "Could not fetch config.");
return;
}
config.Database.Password = CheckForNullOrEmpty(config.Database.Password);
config.Authentication.OAuth2.ClientSecret = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientSecret);
config.Authentication.OAuth2.Secret = CheckForNullOrEmpty(config.Authentication.OAuth2.Secret);
config.Authentication.Secret = CheckForNullOrEmpty(config.Authentication.Secret);
config.Authentication.OAuth2.ClientId = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientId);
await archive.AddText(
"core/config.txt",
JsonSerializer.Serialize(
config,
new JsonSerializerOptions()
{
WriteIndented = true
}
)
);
}
}

View File

@@ -1,22 +0,0 @@
using System.IO.Compression;
using Moonlight.ApiServer.Extensions;
using Moonlight.ApiServer.Interfaces;
namespace Moonlight.ApiServer.Implementations.Diagnose;
public class LogsDiagnoseProvider : IDiagnoseProvider
{
public async Task ModifyZipArchive(ZipArchive archive)
{
var path = Path.Combine("storage", "logs", "latest.log");
if (!File.Exists(path))
{
await archive.AddText("logs.txt", "Logs file latest.log has not been found");
return;
}
var logsContent = await File.ReadAllTextAsync(path);
await archive.AddText("logs.txt", logsContent);
}
}

View File

@@ -1,106 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Interfaces;
using Moonlight.Shared.Http.Responses.OAuth2;
namespace Moonlight.ApiServer.Implementations;
public class LocalOAuth2Provider : IOAuth2Provider
{
private readonly AppConfiguration Configuration;
private readonly ILogger<LocalOAuth2Provider> Logger;
private readonly DatabaseRepository<User> UserRepository;
public LocalOAuth2Provider(
AppConfiguration configuration,
ILogger<LocalOAuth2Provider> logger,
DatabaseRepository<User> userRepository
)
{
UserRepository = userRepository;
Configuration = configuration;
Logger = logger;
}
public Task<string> Start()
{
var redirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect)
? Configuration.PublicUrl
: Configuration.Authentication.OAuth2.AuthorizationRedirect;
var endpoint = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationEndpoint)
? Configuration.PublicUrl + "/oauth2/authorize"
: Configuration.Authentication.OAuth2.AuthorizationEndpoint;
var clientId = Configuration.Authentication.OAuth2.ClientId;
var url = $"{endpoint}" +
$"?client_id={clientId}" +
$"&redirect_uri={redirectUri}" +
$"&response_type=code";
return Task.FromResult(url);
}
public async Task<User?> Complete(string code)
{
// Create http client to call the auth provider
var httpClient = new HttpClient();
using var httpApiClient = new HttpApiClient(httpClient);
httpClient.DefaultRequestHeaders.Add("Authorization",
$"Basic {Configuration.Authentication.OAuth2.ClientSecret}");
// Build access endpoint
var accessEndpoint = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AccessEndpoint)
? $"{Configuration.PublicUrl}/oauth2/handle"
: Configuration.Authentication.OAuth2.AccessEndpoint;
// Build redirect uri
var redirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect)
? Configuration.PublicUrl
: Configuration.Authentication.OAuth2.AuthorizationRedirect;
// Call the auth provider
OAuth2HandleResponse handleData;
try
{
handleData = await httpApiClient.PostJson<OAuth2HandleResponse>(accessEndpoint, new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("redirect_uri", redirectUri),
new KeyValuePair<string, string>("client_id", Configuration.Authentication.OAuth2.ClientId)
]
));
}
catch (HttpApiException e)
{
if (e.Status == 400)
Logger.LogTrace("The auth server returned an error: {e}", e);
else
Logger.LogCritical("The auth server returned an error: {e}", e);
throw new HttpApiException("Unable to request user data", 500);
}
// Notice: We just look up the user id here
// which works as our oauth2 provider is using the same db.
// a real oauth2 provider would create a user here
// Handle the returned data
var userId = handleData.UserId;
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == userId);
return user;
}
}

View File

@@ -1,36 +0,0 @@
using System.Diagnostics.Metrics;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer.Implementations.Metrics;
public class ApplicationMetric : IMetric
{
private Gauge<long> MemoryUsage;
private Gauge<int> CpuUsage;
private Gauge<double> Uptime;
public Task Initialize(Meter meter)
{
MemoryUsage = meter.CreateGauge<long>("moonlight_memory_usage");
CpuUsage = meter.CreateGauge<int>("moonlight_cpu_usage");
Uptime = meter.CreateGauge<double>("moonlight_uptime");
return Task.CompletedTask;
}
public async Task Run(IServiceProvider provider, CancellationToken cancellationToken)
{
var applicationService = provider.GetRequiredService<ApplicationService>();
var memory = await applicationService.GetMemoryUsage();
MemoryUsage.Record(memory);
var uptime = await applicationService.GetUptime();
Uptime.Record(uptime.TotalSeconds);
var cpu = await applicationService.GetCpuUsage();
CpuUsage.Record(cpu);
}
}

View File

@@ -1,28 +0,0 @@
using System.Diagnostics.Metrics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Interfaces;
namespace Moonlight.ApiServer.Implementations.Metrics;
public class UsersMetric : IMetric
{
private Gauge<int> Users;
public Task Initialize(Meter meter)
{
Users = meter.CreateGauge<int>("moonlight_users");
return Task.CompletedTask;
}
public async Task Run(IServiceProvider provider, CancellationToken cancellationToken)
{
var usersRepo = provider.GetRequiredService<DatabaseRepository<User>>();
var count = await usersRepo.Get().CountAsync(cancellationToken: cancellationToken);
Users.Record(count);
}
}

View File

@@ -1,130 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Implementations.Diagnose;
using Moonlight.ApiServer.Implementations.Metrics;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Models;
using Moonlight.ApiServer.Plugins;
using Moonlight.ApiServer.Services;
using OpenTelemetry.Metrics;
namespace Moonlight.ApiServer.Implementations.Startup;
public class CoreStartup : IPluginStartup
{
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
{
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
#region Api Docs
if (configuration.Development.EnableApiDocs)
{
builder.Services.AddEndpointsApiExplorer();
// Configure swagger api specification generator and set the document title for the api docs to use
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("main", new OpenApiInfo()
{
Title = "Moonlight API"
});
options.CustomSchemaIds(x => x.FullName);
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
});
}
#endregion
#region Database
builder.Services.AddDbContext<CoreDataContext>();
#endregion
#region Diagnose
builder.Services.AddSingleton<IDiagnoseProvider, CoreConfigDiagnoseProvider>();
builder.Services.AddSingleton<IDiagnoseProvider, LogsDiagnoseProvider>();
#endregion
#region Prometheus
if (configuration.Metrics.Enable)
{
builder.Services.AddSingleton<MetricsBackgroundService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<MetricsBackgroundService>());
builder.Services.AddSingleton<IMetric, ApplicationMetric>();
builder.Services.AddSingleton<IMetric, UsersMetric>();
builder.Services.AddOpenTelemetry()
.WithMetrics(providerBuilder =>
{
providerBuilder.AddPrometheusExporter();
providerBuilder.AddAspNetCoreInstrumentation();
providerBuilder.AddMeter("moonlight");
});
}
#endregion
#region Client / Frontend
if (configuration.Frontend.EnableHosting)
{
builder.Services.AddSingleton(new FrontendConfigurationOption()
{
Scripts =
[
"/_content/Moonlight.Client/js/moonlight.js", "/_content/MoonCore.Blazor.FlyonUi/moonCore.js",
"/_content/MoonCore.Blazor.FlyonUi/ace/ace.js"
],
Styles = ["/css/style.min.css"]
});
}
#endregion
return Task.CompletedTask;
}
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app)
{
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
#region Prometheus
if (configuration.Metrics.Enable)
app.UseOpenTelemetryPrometheusScrapingEndpoint();
#endregion
return Task.CompletedTask;
}
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder)
{
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
if (configuration.Development.EnableApiDocs)
routeBuilder.MapSwagger("/api/swagger/{documentName}");
return Task.CompletedTask;
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.Extensions.Logging;
using TickerQ.Utilities.Enums;
using TickerQ.Utilities.Interfaces;
namespace Moonlight.ApiServer.Implementations;
public class TickerExceptionHandler : ITickerExceptionHandler
{
private readonly ILogger<TickerExceptionHandler> Logger;
public TickerExceptionHandler(ILogger<TickerExceptionHandler> logger)
{
Logger = logger;
}
public Task HandleExceptionAsync(Exception exception, Guid tickerId, TickerType tickerType)
{
Logger.LogError(exception, "An unhandled error occured while running ticker {id} ({type})", tickerId, tickerType);
return Task.CompletedTask;
}
public Task HandleCanceledExceptionAsync(Exception exception, Guid tickerId, TickerType tickerType)
{
Logger.LogError(exception, "An unhandled error occured while handling canceled ticker {id} ({type})", tickerId, tickerType);
return Task.CompletedTask;
}
}

View File

@@ -1,62 +0,0 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.JwtInvalidation;
using Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Implementations;
public class UserAuthInvalidation : IJwtInvalidateHandler
{
private readonly DatabaseRepository<User> UserRepository;
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
public UserAuthInvalidation(
DatabaseRepository<User> userRepository,
DatabaseRepository<ApiKey> apiKeyRepository
)
{
UserRepository = userRepository;
ApiKeyRepository = apiKeyRepository;
}
public async Task<bool> Handle(ClaimsPrincipal principal)
{
var userIdClaim = principal.FindFirstValue("userId");
if (!string.IsNullOrEmpty(userIdClaim))
{
var userId = int.Parse(userIdClaim);
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == userId);
if (user == null)
return true; // User is deleted, invalidate session
var iatStr = principal.FindFirstValue("iat")!;
var iat = DateTimeOffset.FromUnixTimeSeconds(long.Parse(iatStr));
// If the token has been issued before the token valid time, its expired, and we want to invalidate it
return user.TokenValidTimestamp > iat;
}
var apiKeyIdClaim = principal.FindFirstValue("apiKeyId");
if (!string.IsNullOrEmpty(apiKeyIdClaim))
{
var apiKeyId = int.Parse(apiKeyIdClaim);
var apiKey = await ApiKeyRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == apiKeyId);
// If the api key exists, we don't want to invalidate the request.
// If it doesn't exist we want to invalidate the request
return apiKey == null;
}
return true;
}
}

View File

@@ -1,8 +0,0 @@
using System.IO.Compression;
namespace Moonlight.ApiServer.Interfaces;
public interface IDiagnoseProvider
{
public Task ModifyZipArchive(ZipArchive archive);
}

View File

@@ -1,9 +0,0 @@
using System.Diagnostics.Metrics;
namespace Moonlight.ApiServer.Interfaces;
public interface IMetric
{
public Task Initialize(Meter meter);
public Task Run(IServiceProvider provider, CancellationToken cancellationToken);
}

View File

@@ -1,10 +0,0 @@
using Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Interfaces;
public interface IOAuth2Provider
{
public Task<string> Start();
public Task<User?> Complete(string code);
}

View File

@@ -1,10 +0,0 @@
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Models;
namespace Moonlight.ApiServer.Interfaces;
public interface IUserDeleteHandler
{
public Task<UserDeleteValidationResult> Validate(User user);
public Task Delete(User user, bool force);
}

View File

@@ -1,14 +0,0 @@
using Moonlight.ApiServer.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Sys.Theme;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions;
namespace Moonlight.ApiServer.Mappers;
[Mapper]
public static partial class ThemeMapper
{
public static partial ThemeResponse ToResponse(Theme theme);
public static partial Theme ToTheme(CreateThemeRequest request);
public static partial void Merge([MappingTarget] Theme theme, UpdateThemeRequest request);
}

View File

@@ -1,43 +0,0 @@
namespace Moonlight.ApiServer.Models;
// From https://github.com/scalar/scalar/blob/main/packages/scalar.aspnetcore/ScalarOptions.cs
public class ApiDocsOptions
{
public string Theme { get; set; } = "purple";
public bool? DarkMode { get; set; }
public bool? HideDownloadButton { get; set; }
public bool? ShowSideBar { get; set; }
public bool? WithDefaultFonts { get; set; }
public string? Layout { get; set; }
public string? CustomCss { get; set; }
public string? SearchHotkey { get; set; }
public Dictionary<string, string>? Metadata { get; set; }
public ScalarAuthenticationOptions? Authentication { get; set; }
}
public class ScalarAuthenticationOptions
{
public string? PreferredSecurityScheme { get; set; }
public ScalarAuthenticationApiKey? ApiKey { get; set; }
}
public class ScalarAuthenticationoAuth2
{
public string? ClientId { get; set; }
public List<string>? Scopes { get; set; }
}
public class ScalarAuthenticationApiKey
{
public string? Token { get; set; }
}

View File

@@ -1,49 +0,0 @@
namespace Moonlight.ApiServer.Models;
public class ApplicationTheme
{
public string ColorBackground { get; set; }
public string ColorBase100 { get; set; }
public string ColorBase150 { get; set; }
public string ColorBase200 { get; set; }
public string ColorBase250 { get; set; }
public string ColorBase300 { get; set; }
public string ColorBaseContent { get; set; }
public string ColorPrimary { get; set; }
public string ColorPrimaryContent { get; set; }
public string ColorSecondary { get; set; }
public string ColorSecondaryContent { get; set; }
public string ColorAccent { get; set; }
public string ColorAccentContent { get; set; }
public string ColorNeutral { get; set; }
public string ColorNeutralContent { get; set; }
public string ColorInfo { get; set; }
public string ColorInfoContent { get; set; }
public string ColorSuccess { get; set; }
public string ColorSuccessContent { get; set; }
public string ColorWarning { get; set; }
public string ColorWarningContent { get; set; }
public string ColorError { get; set; }
public string ColorErrorContent { get; set; }
public float RadiusSelector { get; set; }
public float RadiusField { get; set; }
public float RadiusBox { get; set; }
public float SizeSelector { get; set; }
public float SizeField { get; set; }
public float Border { get; set; }
public int Depth { get; set; }
public int Noise { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace Moonlight.ApiServer.Models;
public class FrontendConfigurationOption
{
public string[] Scripts { get; set; } = [];
public string[] Styles { get; set; } = [];
}

View File

@@ -1,27 +0,0 @@
namespace Moonlight.ApiServer.Models;
public class UserDeleteValidationResult
{
public bool IsAllowed { get; set; }
public string Reason { get; set; }
public static UserDeleteValidationResult Allow()
{
return new UserDeleteValidationResult()
{
IsAllowed = true
};
}
public static UserDeleteValidationResult Deny()
=> Deny("No reason provided");
public static UserDeleteValidationResult Deny(string reason)
{
return new UserDeleteValidationResult()
{
IsAllowed = false,
Reason = reason
};
}
}

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Migrations\" />
<Folder Include="Helpers\" />
</ItemGroup>
<PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.7</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<PackageTags>apiserver</PackageTags>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
<PackageReference Include="MoonCore" Version="1.9.2" />
<PackageReference Include="MoonCore.Extended" Version="1.3.5" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="Riok.Mapperly" Version="4.3.0-next.2" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.2" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="TickerQ" Version="2.4.4" />
<PackageReference Include="TickerQ.EntityFrameworkCore" Version="2.4.4" />
</ItemGroup>
<ItemGroup>
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />
<None Remove="Properties\launchSettings.json" />
</ItemGroup>
</Project>

View File

@@ -1,12 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
namespace Moonlight.ApiServer.Plugins;
public interface IPluginStartup
{
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder);
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app);
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder);
}

View File

@@ -1,53 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Attributes;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Services;
[Singleton]
public class ApiKeyService
{
private readonly AppConfiguration Configuration;
public ApiKeyService(AppConfiguration configuration)
{
Configuration = configuration;
}
public string GenerateJwt(ApiKey apiKey)
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var descriptor = new SecurityTokenDescriptor()
{
Expires = apiKey.ExpiresAt.UtcDateTime,
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now.AddMinutes(-1),
Claims = new Dictionary<string, object>()
{
{
"apiKeyId",
apiKey.Id
},
{
"permissions",
string.Join(";", apiKey.Permissions)
}
},
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration.Authentication.Secret)
),
SecurityAlgorithms.HmacSha256
),
Issuer = Configuration.PublicUrl,
Audience = Configuration.PublicUrl
};
var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(descriptor);
return jwtSecurityTokenHandler.WriteToken(securityToken);
}
}

View File

@@ -1,120 +0,0 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MoonCore.Attributes;
using MoonCore.Helpers;
namespace Moonlight.ApiServer.Services;
[Singleton]
public class ApplicationService
{
private ILogger<ApplicationService> Logger;
private readonly IHost Host;
public ApplicationService(ILogger<ApplicationService> logger, IHost host)
{
Logger = logger;
Host = host;
}
public Task<string> GetOsName()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Windows platform detected
var osVersion = Environment.OSVersion.Version;
return Task.FromResult($"Windows {osVersion.Major}.{osVersion.Minor}.{osVersion.Build}");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var releaseRaw = File
.ReadAllLines("/etc/os-release")
.FirstOrDefault(x => x.StartsWith("PRETTY_NAME="));
if (string.IsNullOrEmpty(releaseRaw))
return Task.FromResult("Linux (unknown release)");
var release = releaseRaw
.Replace("PRETTY_NAME=", "")
.Replace("\"", "");
if(string.IsNullOrEmpty(release))
return Task.FromResult("Linux (unknown release)");
return Task.FromResult(release);
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// macOS platform detected
var osVersion = Environment.OSVersion.Version;
return Task.FromResult($"macOS {osVersion.Major}.{osVersion.Minor}.{osVersion.Build}");
}
// Unknown platform
return Task.FromResult("N/A");
}
public async Task<long> GetMemoryUsage()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var process = Process.GetCurrentProcess();
return process.PrivateMemorySize64;
}
else
{
var lines = await File.ReadAllLinesAsync("/proc/self/smaps");
var kilobytes = 0;
foreach (var line in lines)
{
if(!line.StartsWith("pss:", StringComparison.InvariantCultureIgnoreCase))
continue;
var valueString = line
.Replace("pss:", "", StringComparison.InvariantCultureIgnoreCase)
.Replace("kb", "", StringComparison.InvariantCultureIgnoreCase)
.Trim();
kilobytes += int.Parse(valueString);
}
return ByteConverter.FromKiloBytes(kilobytes).Bytes;
}
}
public Task<TimeSpan> GetUptime()
{
var process = Process.GetCurrentProcess();
var uptime = DateTime.Now - process.StartTime;
return Task.FromResult(uptime);
}
public Task<int> GetCpuUsage()
{
var process = Process.GetCurrentProcess();
var cpuTime = process.TotalProcessorTime;
var wallClockTime = DateTime.UtcNow - process.StartTime.ToUniversalTime();
var cpuUsage = (int)(100.0 * cpuTime.TotalMilliseconds / wallClockTime.TotalMilliseconds / Environment.ProcessorCount);
return Task.FromResult(cpuUsage);
}
public Task Shutdown()
{
Logger.LogCritical("Restart of api server has been requested");
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1));
await Host.StopAsync(CancellationToken.None);
});
return Task.CompletedTask;
}
}

View File

@@ -1,96 +0,0 @@
using Moonlight.ApiServer.Interfaces;
using System.IO.Compression;
using Microsoft.Extensions.Logging;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.ApiServer.Services;
[Scoped]
public class DiagnoseService
{
private readonly IEnumerable<IDiagnoseProvider> DiagnoseProviders;
private readonly ILogger<DiagnoseService> Logger;
public DiagnoseService(
IEnumerable<IDiagnoseProvider> diagnoseProviders,
ILogger<DiagnoseService> logger
)
{
DiagnoseProviders = diagnoseProviders;
Logger = logger;
}
public Task<DiagnoseProvideResponse[]> GetProviders()
{
var availableProviders = new List<DiagnoseProvideResponse>();
foreach (var diagnoseProvider in DiagnoseProviders)
{
var name = diagnoseProvider.GetType().Name;
var type = diagnoseProvider.GetType().FullName;
// The type name is null if the type is a generic type, unlikely, but still could happen
if (type == null)
continue;
availableProviders.Add(new DiagnoseProvideResponse()
{
Name = name,
Type = type
});
}
return Task.FromResult(
availableProviders.ToArray()
);
}
public async Task<MemoryStream> GenerateDiagnose(string[] requestedProviders)
{
IDiagnoseProvider[] providers;
if (requestedProviders.Length == 0)
providers = DiagnoseProviders.ToArray();
else
{
var foundProviders = new List<IDiagnoseProvider>();
foreach (var requestedProvider in requestedProviders)
{
var provider = DiagnoseProviders.FirstOrDefault(x => x.GetType().FullName == requestedProvider);
if (provider == null)
continue;
foundProviders.Add(provider);
}
providers = foundProviders.ToArray();
}
try
{
var outputStream = new MemoryStream();
var zipArchive = new ZipArchive(outputStream, ZipArchiveMode.Create, leaveOpen: true);
foreach (var provider in providers)
{
await provider.ModifyZipArchive(zipArchive);
}
zipArchive.Dispose();
outputStream.Position = 0;
return outputStream;
}
catch (Exception e)
{
Logger.LogError("An unhandled error occured while generated the diagnose file: {e}", e);
throw new HttpApiException("An unhandled error occured while generating the diagnose file", 500);
}
}
}

View File

@@ -1,168 +0,0 @@
using System.IO.Compression;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Http.Controllers.Frontend;
using Moonlight.ApiServer.Models;
using Moonlight.Shared.Misc;
namespace Moonlight.ApiServer.Services;
[Scoped]
public class FrontendService
{
private readonly AppConfiguration Configuration;
private readonly IWebHostEnvironment WebHostEnvironment;
private readonly IEnumerable<FrontendConfigurationOption> ConfigurationOptions;
private readonly IServiceProvider ServiceProvider;
private readonly DatabaseRepository<Theme> ThemeRepository;
public FrontendService(
AppConfiguration configuration,
IWebHostEnvironment webHostEnvironment,
IEnumerable<FrontendConfigurationOption> configurationOptions,
IServiceProvider serviceProvider,
DatabaseRepository<Theme> themeRepository
)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
ConfigurationOptions = configurationOptions;
ServiceProvider = serviceProvider;
ThemeRepository = themeRepository;
}
public Task<FrontendConfiguration> GetConfiguration()
{
var configuration = new FrontendConfiguration()
{
ApiUrl = Configuration.PublicUrl,
HostEnvironment = "ApiServer"
};
return Task.FromResult(configuration);
}
public async Task<string> GenerateIndexHtml() // TODO: Cache
{
// Load requested theme
var theme = await ThemeRepository
.Get()
.FirstOrDefaultAsync(x => x.IsEnabled);
// Load configured javascript files
var scripts = ConfigurationOptions
.SelectMany(x => x.Scripts)
.Distinct()
.ToArray();
// Load configured css files
var styles = ConfigurationOptions
.SelectMany(x => x.Styles)
.Distinct()
.ToArray();
return await ComponentHelper.RenderComponent<FrontendPage>(
ServiceProvider,
parameters =>
{
parameters["Theme"] = theme!;
parameters["Styles"] = styles;
parameters["Scripts"] = scripts;
parameters["Title"] = "Moonlight"; // TODO: Config
}
);
}
public async Task<Stream> GenerateZip() // TODO: Rework to be able to extract everything successfully
{
// We only allow the access to this function when we are actually hosting the frontend
if (!Configuration.Frontend.EnableHosting)
throw new HttpApiException("The hosting of the wasm client has been disabled", 400);
// Load and check wasm path
var wasmMainFile = WebHostEnvironment.WebRootFileProvider.GetFileInfo("index.html");
if (wasmMainFile is NotFoundFileInfo || string.IsNullOrEmpty(wasmMainFile.PhysicalPath))
throw new HttpApiException("Unable to find wasm location", 500);
var wasmPath = Path.GetDirectoryName(wasmMainFile.PhysicalPath)! + "/";
// Load and check the blazor framework files
var blazorFile = WebHostEnvironment.WebRootFileProvider.GetFileInfo("_framework/blazor.webassembly.js");
if (blazorFile is NotFoundFileInfo || string.IsNullOrEmpty(blazorFile.PhysicalPath))
throw new HttpApiException("Unable to find blazor location", 500);
var blazorPath = Path.GetDirectoryName(blazorFile.PhysicalPath)! + "/";
// Create zip
var memoryStream = new MemoryStream();
var zipArchive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true);
// Add wasm application
await ArchiveFsItem(zipArchive, wasmPath, wasmPath);
// Add blazor files
await ArchiveFsItem(zipArchive, blazorPath, blazorPath, "_framework/");
// Add frontend.json
var frontendConfig = await GetConfiguration();
frontendConfig.HostEnvironment = "Static";
var frontendJson = JsonSerializer.Serialize(frontendConfig);
await ArchiveText(zipArchive, "frontend.json", frontendJson);
// Finish zip archive and reset stream so the code calling this function can process it
zipArchive.Dispose();
await memoryStream.FlushAsync();
memoryStream.Position = 0;
return memoryStream;
}
private async Task ArchiveFsItem(ZipArchive archive, string path, string prefixToRemove, string prefixToAdd = "")
{
if (File.Exists(path))
{
var entryName = prefixToAdd + Formatter.ReplaceStart(path, prefixToRemove, "");
var entry = archive.CreateEntry(entryName);
await using var entryStream = entry.Open();
await using var fileStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await fileStream.CopyToAsync(entryStream);
await entryStream.FlushAsync();
entryStream.Close();
}
else
{
foreach (var directoryItem in Directory.EnumerateFileSystemEntries(path))
await ArchiveFsItem(archive, directoryItem, prefixToRemove, prefixToAdd);
}
}
private async Task ArchiveText(ZipArchive archive, string path, string content)
{
var data = Encoding.UTF8.GetBytes(content);
await ArchiveBytes(archive, path, data);
}
private async Task ArchiveBytes(ZipArchive archive, string path, byte[] bytes)
{
var entry = archive.CreateEntry(path);
await using var dataStream = entry.Open();
await dataStream.WriteAsync(bytes);
await dataStream.FlushAsync();
}
}

View File

@@ -1,81 +0,0 @@
using System.Diagnostics.Metrics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Interfaces;
namespace Moonlight.ApiServer.Services;
public class MetricsBackgroundService : BackgroundService
{
private readonly ILogger<MetricsBackgroundService> Logger;
private readonly IServiceProvider ServiceProvider;
private readonly AppConfiguration Configuration;
private readonly IMetric[] Metrics;
private readonly Meter Meter;
public MetricsBackgroundService(
IServiceProvider serviceProvider,
IMeterFactory meterFactory,
IEnumerable<IMetric> metrics,
ILogger<MetricsBackgroundService> logger,
AppConfiguration configuration
)
{
ServiceProvider = serviceProvider;
Logger = logger;
Configuration = configuration;
Meter = meterFactory.Create("moonlight");
Metrics = metrics.ToArray();
}
private async Task Initialize()
{
Logger.LogDebug(
"Initializing metrics: {names}",
string.Join(", ", Metrics.Select(x => x.GetType().FullName))
);
foreach (var metric in Metrics)
await metric.Initialize(Meter);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Initialize();
while (!stoppingToken.IsCancellationRequested)
{
using var scope = ServiceProvider.CreateScope();
foreach (var metric in Metrics)
{
try
{
await metric.Run(scope.ServiceProvider, stoppingToken);
}
catch (TaskCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogError(
"An unhandled error occured while collecting metric {name}: {e}",
metric.GetType().FullName,
e
);
}
}
await Task.Delay(
TimeSpan.FromSeconds(Configuration.Metrics.Interval),
stoppingToken
);
}
}
}

View File

@@ -1,42 +0,0 @@
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Models;
namespace Moonlight.ApiServer.Services;
public class UserDeletionService
{
private readonly IUserDeleteHandler[] Handlers;
private readonly DatabaseRepository<User> UserRepository;
public UserDeletionService(
IEnumerable<IUserDeleteHandler> handlers,
DatabaseRepository<User> userRepository
)
{
UserRepository = userRepository;
Handlers = handlers.ToArray();
}
public async Task<UserDeleteValidationResult> Validate(User user)
{
foreach (var handler in Handlers)
{
var result = await handler.Validate(user);
if (!result.IsAllowed)
return result;
}
return UserDeleteValidationResult.Allow();
}
public async Task Delete(User user, bool force)
{
foreach (var handler in Handlers)
await Delete(user, force);
await UserRepository.Remove(user);
}
}

View File

@@ -1,64 +0,0 @@
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Extended.JwtInvalidation;
using MoonCore.Permissions;
using Moonlight.ApiServer.Implementations;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private Task RegisterAuth()
{
WebApplicationBuilder.Services
.AddAuthentication("coreAuthentication")
.AddJwtBearer("coreAuthentication", options =>
{
options.TokenValidationParameters = new()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
Configuration.Authentication.Secret
)),
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidateAudience = true,
ValidAudience = Configuration.PublicUrl,
ValidateIssuer = true,
ValidIssuer = Configuration.PublicUrl
};
});
WebApplicationBuilder.Services.AddJwtBearerInvalidation("coreAuthentication");
WebApplicationBuilder.Services.AddScoped<IJwtInvalidateHandler, UserAuthInvalidation>();
WebApplicationBuilder.Services.AddAuthorization();
WebApplicationBuilder.Services.AddAuthorizationPermissions(options =>
{
options.ClaimName = "permissions";
options.Prefix = "permissions:";
});
// Add local oauth2 provider if enabled
if (Configuration.Authentication.EnableLocalOAuth2)
WebApplicationBuilder.Services.AddScoped<IOAuth2Provider, LocalOAuth2Provider>();
WebApplicationBuilder.Services.AddScoped<UserDeletionService>();
return Task.CompletedTask;
}
private Task UseAuth()
{
WebApplication.UseAuthentication();
WebApplication.UseAuthorization();
return Task.CompletedTask;
}
}

View File

@@ -1,63 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Extended.Extensions;
using MoonCore.Extensions;
using MoonCore.Helpers;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private Task RegisterBase()
{
WebApplicationBuilder.Services.AutoAddServices<Startup>();
WebApplicationBuilder.Services.AddHttpClient();
WebApplicationBuilder.Services.AddApiExceptionHandler();
// Add pre-existing services
WebApplicationBuilder.Services.AddSingleton(Configuration);
// Configure controllers
var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
// Add plugin assemblies as application parts
foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct())
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
return Task.CompletedTask;
}
private Task UseBase()
{
WebApplication.UseRouting();
WebApplication.UseExceptionHandler();
return Task.CompletedTask;
}
private Task MapBase()
{
WebApplication.MapControllers();
if (Configuration.Frontend.EnableHosting)
WebApplication.MapFallbackToController("Index", "Frontend");
return Task.CompletedTask;
}
private Task ConfigureKestrel()
{
WebApplicationBuilder.WebHost.ConfigureKestrel(kestrelOptions =>
{
var maxUploadInBytes = ByteConverter
.FromMegaBytes(Configuration.Kestrel.UploadLimit)
.Bytes;
kestrelOptions.Limits.MaxRequestBodySize = maxUploadInBytes;
});
return Task.CompletedTask;
}
}

View File

@@ -1,35 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.EnvConfiguration;
using MoonCore.Yaml;
using Moonlight.ApiServer.Configuration;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private async Task SetupAppConfiguration()
{
var configPath = Path.Combine("storage", "config.yml");
await YamlDefaultGenerator.Generate<AppConfiguration>(configPath);
// Configure configuration (wow)
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddYamlFile(configPath);
configurationBuilder.AddEnvironmentVariables(prefix: "MOONLIGHT_", separator: "_");
var configurationRoot = configurationBuilder.Build();
// Retrieve configuration
Configuration = AppConfiguration.CreateEmpty();
configurationRoot.Bind(Configuration);
}
private Task RegisterAppConfiguration()
{
WebApplicationBuilder.Services.AddSingleton(Configuration);
return Task.CompletedTask;
}
}

View File

@@ -1,25 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Extensions;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private Task RegisterDatabase()
{
WebApplicationBuilder.Services.AddDatabaseMappings();
WebApplicationBuilder.Services.AddServiceCollectionAccessor();
WebApplicationBuilder.Services.AddScoped(typeof(DatabaseRepository<>));
return Task.CompletedTask;
}
private async Task PrepareDatabase()
{
await WebApplication.Services.EnsureDatabaseMigrated();
WebApplication.Services.GenerateDatabaseMappings();
}
}

View File

@@ -1,63 +0,0 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MoonCore.Logging;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private Task SetupLogging()
{
var loggerFactory = new LoggerFactory();
loggerFactory.AddAnsiConsole();
Logger = loggerFactory.CreateLogger<Startup>();
return Task.CompletedTask;
}
private async Task RegisterLogging()
{
// Configure application logging
WebApplicationBuilder.Logging.ClearProviders();
WebApplicationBuilder.Logging.AddAnsiConsole();
WebApplicationBuilder.Logging.AddFile(Path.Combine("storage", "logs", "moonlight.log"));
// Logging levels
var logConfigPath = Path.Combine("storage", "logConfig.json");
// Ensure logging config, add a default one is missing
if (!File.Exists(logConfigPath))
{
var defaultLogLevels = new Dictionary<string, string>
{
{ "Default", "Information" },
{ "Microsoft.AspNetCore", "Warning" },
{ "System.Net.Http.HttpClient", "Warning" }
};
var logLevelsJson = JsonSerializer.Serialize(defaultLogLevels);
await File.WriteAllTextAsync(logConfigPath, logLevelsJson);
}
// Add logging configuration
var logLevels = JsonSerializer.Deserialize<Dictionary<string, string>>(
await File.ReadAllTextAsync(logConfigPath)
)!;
foreach (var level in logLevels)
WebApplicationBuilder.Logging.AddFilter(level.Key, Enum.Parse<LogLevel>(level.Value));
// Mute exception handler middleware
// https://github.com/dotnet/aspnetcore/issues/19740
WebApplicationBuilder.Logging.AddFilter(
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
LogLevel.Critical
);
WebApplicationBuilder.Logging.AddFilter(
"Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware",
LogLevel.Critical
);
}
}

View File

@@ -1,73 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private Task PrintVersion()
{
// Fancy start console output... yes very fancy :>
var rainbow = new Crayon.Rainbow(0.5);
foreach (var c in "Moonlight")
{
Console.Write(
rainbow
.Next()
.Bold()
.Text(c.ToString())
);
}
Console.WriteLine();
return Task.CompletedTask;
}
private Task CreateStorage()
{
Directory.CreateDirectory("storage");
Directory.CreateDirectory(Path.Combine("storage", "logs"));
return Task.CompletedTask;
}
private Task RegisterCors()
{
var allowedOrigins = Configuration.Kestrel.AllowedOrigins;
WebApplicationBuilder.Services.AddCors(options =>
{
var cors = new CorsPolicyBuilder();
if (allowedOrigins.Contains("*"))
{
cors.SetIsOriginAllowed(_ => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}
else
{
cors.WithOrigins(allowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
}
options.AddDefaultPolicy(
cors.Build()
);
});
return Task.CompletedTask;
}
private Task UseCors()
{
WebApplication.UseCors();
return Task.CompletedTask;
}
}

View File

@@ -1,87 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MoonCore.Logging;
using Moonlight.ApiServer.Plugins;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private IServiceProvider PluginLoadServiceProvider;
private IPluginStartup[] PluginStartups;
private Task InitializePlugins()
{
// Create service provider for starting up
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(Configuration);
serviceCollection.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddAnsiConsole();
});
PluginLoadServiceProvider = serviceCollection.BuildServiceProvider();
return Task.CompletedTask;
}
private async Task HookPluginBuild()
{
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'BuildApp' for '{name}': {e}",
pluginAppStartup.GetType().FullName,
e
);
}
}
}
private async Task HookPluginConfigure()
{
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'ConfigureApp' for '{name}': {e}",
pluginAppStartup.GetType().FullName,
e
);
}
}
}
private async Task HookPluginEndpoints()
{
foreach (var pluginEndpointStartup in PluginStartups)
{
try
{
await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'ConfigureEndpoints' for '{name}': {e}",
pluginEndpointStartup.GetType().FullName,
e
);
}
}
}
}

View File

@@ -1,34 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Implementations;
using TickerQ.DependencyInjection;
using TickerQ.EntityFrameworkCore.DependencyInjection;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private Task RegisterTickerQ()
{
WebApplicationBuilder.Services.AddTickerQ(builder =>
{
builder.SetExceptionHandler<TickerExceptionHandler>();
builder.AddOperationalStore<TickerDataContext>(optionBuilder =>
{
optionBuilder.CancelMissedTickersOnApplicationRestart();
});
});
WebApplicationBuilder.Services.AddDbContext<TickerDataContext>();
return Task.CompletedTask;
}
private Task UseTickerQ()
{
WebApplication.UseTickerQ();
return Task.CompletedTask;
}
}

View File

@@ -1,67 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Logging;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Plugins;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private string[] Args;
// Logger
public ILogger<Startup> Logger { get; private set; }
// Configuration
public AppConfiguration Configuration { get; private set; }
// WebApplication Stuff
public WebApplication WebApplication { get; private set; }
public WebApplicationBuilder WebApplicationBuilder { get; private set; }
public Task Initialize(string[] args, IPluginStartup[]? plugins = null)
{
Args = args;
PluginStartups = plugins ?? [];
return Task.CompletedTask;
}
public async Task AddMoonlight(WebApplicationBuilder builder)
{
WebApplicationBuilder = builder;
await PrintVersion();
await CreateStorage();
await SetupAppConfiguration();
await SetupLogging();
await InitializePlugins();
await ConfigureKestrel();
await RegisterAppConfiguration();
await RegisterLogging();
await RegisterBase();
await RegisterDatabase();
await RegisterAuth();
await RegisterCors();
await RegisterTickerQ();
await HookPluginBuild();
}
public async Task AddMoonlight(WebApplication application)
{
WebApplication = application;
await PrepareDatabase();
await UseCors();
await UseBase();
await UseAuth();
await UseTickerQ();
await HookPluginConfigure();
await MapBase();
await HookPluginEndpoints();
}
}

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