Compare commits
19 Commits
v2_ChangeA
...
v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39a1bdeb5d | ||
|
|
18f107e485 | ||
|
|
b56b94041f | ||
|
|
1a3b339983 | ||
|
|
106af71c17 | ||
|
|
cb1da9f5cd | ||
|
|
743b0b1c1b | ||
|
|
8fe695f0cf | ||
|
|
344ea046f2 | ||
|
|
73a014500d | ||
|
|
d3f8ba5434 | ||
|
|
c068679f9b | ||
|
|
9dfba1e6c4 | ||
|
|
27f9ca551c | ||
|
|
4d69e8bacb | ||
|
|
ed2d33c69b | ||
|
|
d880189f2b | ||
|
|
9221a66597 | ||
|
|
e1c4f8a2a9 |
20
.devcontainers/devcontainer.json
Normal file
20
.devcontainers/devcontainer.json
Normal 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
25
.dockerignore
Normal 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
8
.gitattributes
vendored
@@ -1,2 +1,10 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
# Auto detect text files and perform LF normalization
|
||||||
* text=auto
|
* 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
|
||||||
|
|||||||
15
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
15
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -33,7 +33,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Panel Version
|
label: Panel Version
|
||||||
description: Version number of your Panel (latest is not a version)
|
description: Version number of your Panel (latest is not a version)
|
||||||
placeholder: v2 EA
|
placeholder: 1.4.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -42,7 +42,16 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Daemon Version
|
label: Daemon Version
|
||||||
description: Version number of your Daemon (latest is not a 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:
|
validations:
|
||||||
required: true
|
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.
|
- 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
|
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.
|
- 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
12
.github/dependabot.yml
vendored
Normal 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"
|
||||||
27
.github/workflows/canary-docker-build.yml
vendored
Normal file
27
.github/workflows/canary-docker-build.yml
vendored
Normal 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
|
||||||
42
.github/workflows/publish-dev-packages.yml
vendored
42
.github/workflows/publish-dev-packages.yml
vendored
@@ -1,42 +0,0 @@
|
|||||||
name: Build and Publish NuGet Package
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ v2_ChangeArchitecture,v2.1 ]
|
|
||||||
paths:
|
|
||||||
- '**.csproj'
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: debian-12
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
project:
|
|
||||||
- Moonlight.Client
|
|
||||||
- Moonlight.ApiServer
|
|
||||||
- Moonlight.Shared
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# Step 1: Clean environment
|
|
||||||
- name: Clean up Environment
|
|
||||||
run: |
|
|
||||||
rm -rf ./*
|
|
||||||
rm -rf ./.??*
|
|
||||||
|
|
||||||
# Step 2: Checkout the code
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Step 3: Build project
|
|
||||||
- name: "Build project"
|
|
||||||
run: dotnet build --configuration Debug ${{ matrix.project }}/${{ matrix.project }}.csproj
|
|
||||||
|
|
||||||
# Step 4: Pack project
|
|
||||||
- name: "Pack project"
|
|
||||||
run: dotnet pack --configuration Debug --no-build --output . ${{ matrix.project }}/${{ matrix.project }}.csproj
|
|
||||||
|
|
||||||
# Step 5: Publish on package registry
|
|
||||||
- name: Publish on package registry"
|
|
||||||
run: dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{secrets.GH_PACKAGES_READWRITE}} --source https://nuget.pkg.github.com/Moonlight-Panel/index.json
|
|
||||||
25
.github/workflows/release-docker-build.yml
vendored
Normal file
25
.github/workflows/release-docker-build.yml
vendored
Normal 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
18
.github/workflows/test-docker-build.yml
vendored
Normal 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
469
.gitignore
vendored
@@ -1,434 +1,45 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
Common IntelliJ Platform excludes
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
##
|
# User specific
|
||||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
**/.idea/**/workspace.xml
|
||||||
|
**/.idea/**/tasks.xml
|
||||||
# User-specific files
|
**/.idea/shelf/*
|
||||||
*.rsuser
|
**/.idea/dictionaries
|
||||||
*.suo
|
**/.idea/httpRequests/
|
||||||
*.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
|
|
||||||
|
|
||||||
# Sensitive or high-churn files
|
# Sensitive or high-churn files
|
||||||
.idea/**/dataSources/
|
**/.idea/**/dataSources/
|
||||||
.idea/**/dataSources.ids
|
**/.idea/**/dataSources.ids
|
||||||
.idea/**/dataSources.local.xml
|
**/.idea/**/dataSources.xml
|
||||||
.idea/**/sqlDataSources.xml
|
**/.idea/**/dataSources.local.xml
|
||||||
.idea/**/dynamic.xml
|
**/.idea/**/sqlDataSources.xml
|
||||||
.idea/**/uiDesigner.xml
|
**/.idea/**/dynamic.xml
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
|
|
||||||
# Gradle
|
# Rider
|
||||||
.idea/**/gradle.xml
|
# Rider auto-generates .iml files, and contentModel.xml
|
||||||
.idea/**/libraries
|
**/.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/
|
storage/
|
||||||
**/.idea/**
|
Moonlight/publish.ps1
|
||||||
style.min.css
|
Moonlight/version
|
||||||
|
|
||||||
# Build script for nuget packages
|
|
||||||
finalPackages/
|
|
||||||
nupkgs/
|
|
||||||
|
|
||||||
# Scripts
|
|
||||||
**/bin/**
|
|
||||||
**/obj/**
|
|
||||||
|
|||||||
13
.idea/.idea.Moonlight/.idea/.gitignore
generated
vendored
Normal file
13
.idea/.idea.Moonlight/.idea/.gitignore
generated
vendored
Normal 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
|
||||||
21
.idea/.idea.Moonlight/.idea/efCoreCommonOptions.xml
generated
Normal file
21
.idea/.idea.Moonlight/.idea/efCoreCommonOptions.xml
generated
Normal 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>
|
||||||
14
.idea/.idea.Moonlight/.idea/efCoreDialogsState.xml
generated
Normal file
14
.idea/.idea.Moonlight/.idea/efCoreDialogsState.xml
generated
Normal 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>
|
||||||
4
.idea/.idea.Moonlight/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.Moonlight/.idea/encodings.xml
generated
Normal 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>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
8
.idea/.idea.Moonlight/.idea/indexLayout.xml
generated
Normal file
8
.idea/.idea.Moonlight/.idea/indexLayout.xml
generated
Normal 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
6
.idea/.idea.Moonlight/.idea/vcs.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<values>
|
||||||
|
<value name="mac.address" type="string">a6a05ab0b1614c08281b54fc3b3339170b0f57a5e246c20b7393333dfa28f8f1</value>
|
||||||
|
</values>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<values>
|
||||||
|
<value name="StillAlive" type="qword">133564417297189136</value>
|
||||||
|
</values>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<values>
|
||||||
|
<value name="MachineId" type="string">{A6A05AB0-B161-4C08-281B-54FC3B333917}</value>
|
||||||
|
</values>
|
||||||
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal 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
41
.vscode/tasks.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,26 +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.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Import Project="Plugins.props" />
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using MoonCore.PluginFramework;
|
|
||||||
using Moonlight.ApiServer.Plugins;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Runtime;
|
|
||||||
|
|
||||||
[PluginLoader]
|
|
||||||
public partial class PluginLoader : IPluginStartup
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<ItemGroup>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Runtime;
|
|
||||||
using Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
var pluginLoader = new PluginLoader();
|
|
||||||
pluginLoader.Initialize();
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.AddMoonlight(pluginLoader.Instances);
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
app.UseMoonlight(pluginLoader.Instances);
|
|
||||||
|
|
||||||
// Add frontend
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
builder.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
// Handle setup of wasm app hosting in the runtime
|
|
||||||
// so the Moonlight.ApiServer doesn't need the wasm package
|
|
||||||
if (configuration.Frontend.EnableHosting)
|
|
||||||
{
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
app.UseWebAssemblyDebugging();
|
|
||||||
|
|
||||||
app.UseBlazorFrameworkFiles();
|
|
||||||
app.UseStaticFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.MapMoonlight(pluginLoader.Instances);
|
|
||||||
|
|
||||||
|
|
||||||
await app.RunAsync();
|
|
||||||
@@ -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}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Implementations.LocalAuth;
|
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Configuration;
|
|
||||||
|
|
||||||
public record AppConfiguration
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "Moonlight configuration\n\n\nThe public url your instance should be accessible through")]
|
|
||||||
public string PublicUrl { get; set; } = "http://localhost:5165";
|
|
||||||
|
|
||||||
[YamlMember(Description = "\nThe credentials of the postgres which moonlight should use")]
|
|
||||||
public DatabaseConfig Database { get; set; } = new();
|
|
||||||
|
|
||||||
[YamlMember(Description = "\nSettings regarding authentication")]
|
|
||||||
public AuthenticationConfig Authentication { get; set; } = new();
|
|
||||||
|
|
||||||
[YamlMember(Description = "\nThese options are only meant for development purposes")]
|
|
||||||
public DevelopmentConfig Development { get; set; } = new();
|
|
||||||
|
|
||||||
[YamlMember(Description = "\nSettings for hosting the frontend")]
|
|
||||||
public FrontendData Frontend { get; set; } = new();
|
|
||||||
|
|
||||||
[YamlMember(Description = "\nSettings for the internal web server moonlight is running in")]
|
|
||||||
public KestrelConfig Kestrel { get; set; } = new();
|
|
||||||
|
|
||||||
[YamlMember(Description = "\nSettings for the internal file manager for moonlights storage access")]
|
|
||||||
public FilesData Files { get; set; } = new();
|
|
||||||
|
|
||||||
[YamlMember(Description = "\nSettings for open telemetry")]
|
|
||||||
public OpenTelemetryData OpenTelemetry { get; set; } = new();
|
|
||||||
|
|
||||||
[YamlMember(Description = "\nConfiguration for the realtime communication solution SignalR")]
|
|
||||||
public SignalRData SignalR { get; set; } = new();
|
|
||||||
|
|
||||||
public static AppConfiguration CreateEmpty()
|
|
||||||
{
|
|
||||||
return new AppConfiguration()
|
|
||||||
{
|
|
||||||
// Set arrays as empty here
|
|
||||||
|
|
||||||
Kestrel = new()
|
|
||||||
{
|
|
||||||
AllowedOrigins = []
|
|
||||||
},
|
|
||||||
Authentication = new()
|
|
||||||
{
|
|
||||||
EnabledSchemes = []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public record SignalRData
|
|
||||||
{
|
|
||||||
[YamlMember(Description =
|
|
||||||
"\nWhether to use redis (or any other redis compatible solution) to scale out SignalR hubs. This is required when using multiple api server replicas")]
|
|
||||||
public bool UseRedis { get; set; } = false;
|
|
||||||
|
|
||||||
public string RedisConnectionString { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public record FilesData
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "The maximum file size limit a combine operation is allowed to process")]
|
|
||||||
public double CombineLimit { get; set; } = ByteConverter.FromGigaBytes(5).MegaBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record FrontendData
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "Enable the hosting of the frontend. Disable this if you only want to run the api server")]
|
|
||||||
public bool EnableHosting { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DatabaseConfig
|
|
||||||
{
|
|
||||||
public string Host { get; set; } = "your-database-host.name";
|
|
||||||
public int Port { get; set; } = 5432;
|
|
||||||
|
|
||||||
public string Username { get; set; } = "db_user";
|
|
||||||
public string Password { get; set; } = "db_password";
|
|
||||||
|
|
||||||
public string Database { get; set; } = "db_name";
|
|
||||||
}
|
|
||||||
|
|
||||||
public record AuthenticationConfig
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "The secret token to use for creating jwts and encrypting things. This needs to be at least 32 characters long")]
|
|
||||||
public string Secret { get; set; } = Formatter.GenerateString(32);
|
|
||||||
|
|
||||||
[YamlMember(Description = "Settings for the user sessions")]
|
|
||||||
public SessionsConfig Sessions { get; set; } = new();
|
|
||||||
|
|
||||||
[YamlMember(Description = "This specifies if the first registered/synced user will become an admin automatically")]
|
|
||||||
public bool FirstUserAdmin { get; set; } = true;
|
|
||||||
|
|
||||||
[YamlMember(Description = "This specifies the authentication schemes the frontend should be able to challenge")]
|
|
||||||
public string[] EnabledSchemes { get; set; } = [LocalAuthConstants.AuthenticationScheme];
|
|
||||||
}
|
|
||||||
|
|
||||||
public record SessionsConfig
|
|
||||||
{
|
|
||||||
public string CookieName { get; set; } = "session";
|
|
||||||
public int ExpiresIn { get; set; } = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DevelopmentConfig
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "This toggles the availability of the api docs via /api/swagger")]
|
|
||||||
public bool EnableApiDocs { get; set; } = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record KestrelConfig
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "The upload limit in megabytes for the api server")]
|
|
||||||
public int UploadLimit { get; set; } = 100;
|
|
||||||
|
|
||||||
[YamlMember(Description = "The allowed origins for the api server. Use * to allow all origins (which is not advised)")]
|
|
||||||
public string[] AllowedOrigins { get; set; } = ["*"];
|
|
||||||
}
|
|
||||||
|
|
||||||
public record OpenTelemetryData
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "This enables open telemetry for moonlight")]
|
|
||||||
public bool Enable { get; set; } = false;
|
|
||||||
|
|
||||||
public OpenTelemetryMetricsData Metrics { get; set; } = new();
|
|
||||||
public OpenTelemetryTracesData Traces { get; set; } = new();
|
|
||||||
public OpenTelemetryLogsData Logs { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public record OpenTelemetryMetricsData
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "This enables the exporting of metrics")]
|
|
||||||
public bool Enable { get; set; } = true;
|
|
||||||
|
|
||||||
[YamlMember(Description = "Enables the /metrics exporter for prometheus")]
|
|
||||||
public bool EnablePrometheus { get; set; } = false;
|
|
||||||
|
|
||||||
[YamlMember(Description = "The interval in which metrics are created, specified in seconds")]
|
|
||||||
public int Interval { get; set; } = 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record OpenTelemetryTracesData
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "This enables the exporting of traces")]
|
|
||||||
public bool Enable { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record OpenTelemetryLogsData
|
|
||||||
{
|
|
||||||
[YamlMember(Description = "This enables the exporting of logs")]
|
|
||||||
public bool Enable { get; set; } = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using Hangfire.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.ApiServer.Models;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Database;
|
|
||||||
|
|
||||||
public class CoreDataContext : DbContext
|
|
||||||
{
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
|
|
||||||
public DbSet<User> Users { get; set; }
|
|
||||||
public DbSet<ApiKey> ApiKeys { get; set; }
|
|
||||||
public DbSet<Theme> Themes { get; set; }
|
|
||||||
|
|
||||||
public CoreDataContext(AppConfiguration configuration)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
||||||
{
|
|
||||||
if(optionsBuilder.IsConfigured)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var database = Configuration.Database;
|
|
||||||
|
|
||||||
var connectionString = $"Host={database.Host};" +
|
|
||||||
$"Port={database.Port};" +
|
|
||||||
$"Database={database.Database};" +
|
|
||||||
$"Username={database.Username};" +
|
|
||||||
$"Password={database.Password}";
|
|
||||||
|
|
||||||
optionsBuilder.UseNpgsql(connectionString, builder =>
|
|
||||||
{
|
|
||||||
builder.MigrationsHistoryTable("MigrationsHistory", "core");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Model.SetDefaultSchema("core");
|
|
||||||
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
modelBuilder.OnHangfireModelCreating();
|
|
||||||
|
|
||||||
modelBuilder.Ignore<ApplicationTheme>();
|
|
||||||
modelBuilder.Entity<Theme>()
|
|
||||||
.OwnsOne(x => x.Content, builder =>
|
|
||||||
{
|
|
||||||
builder.ToJson();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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; } = [];
|
|
||||||
}
|
|
||||||
@@ -1,565 +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("20250919201409_RecreatedMigrationsForChangeOfSchema")]
|
|
||||||
partial class RecreatedMigrationsForChangeOfSchema
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasDefaultSchema("core")
|
|
||||||
.HasAnnotation("ProductVersion", "9.0.8")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
|
|
||||||
{
|
|
||||||
b.Property<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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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("ApiKeys", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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("Themes", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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("Users", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("StateId");
|
|
||||||
|
|
||||||
b.Navigation("State");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
|
|
||||||
.WithMany("Parameters")
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
|
|
||||||
.WithMany("QueuedJobs")
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
|
|
||||||
.WithMany("States")
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
|
|
||||||
{
|
|
||||||
b.OwnsOne("Moonlight.ApiServer.Models.ApplicationTheme", "Content", b1 =>
|
|
||||||
{
|
|
||||||
b1.Property<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("Themes", "core");
|
|
||||||
|
|
||||||
b1.ToJson("Content");
|
|
||||||
|
|
||||||
b1.WithOwner()
|
|
||||||
.HasForeignKey("ThemeId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.Navigation("Content")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Parameters");
|
|
||||||
|
|
||||||
b.Navigation("QueuedJobs");
|
|
||||||
|
|
||||||
b.Navigation("States");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class RecreatedMigrationsForChangeOfSchema : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.EnsureSchema(
|
|
||||||
name: "core");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ApiKeys",
|
|
||||||
schema: "core",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Description = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Permissions = table.Column<string[]>(type: "text[]", nullable: false),
|
|
||||||
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_ApiKeys", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "HangfireCounter",
|
|
||||||
schema: "core",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<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",
|
|
||||||
schema: "core",
|
|
||||||
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",
|
|
||||||
schema: "core",
|
|
||||||
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",
|
|
||||||
schema: "core",
|
|
||||||
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",
|
|
||||||
schema: "core",
|
|
||||||
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",
|
|
||||||
schema: "core",
|
|
||||||
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: "Themes",
|
|
||||||
schema: "core",
|
|
||||||
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_Themes", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Users",
|
|
||||||
schema: "core",
|
|
||||||
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<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
Permissions = table.Column<string[]>(type: "text[]", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Users", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "HangfireJob",
|
|
||||||
schema: "core",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<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",
|
|
||||||
schema: "core",
|
|
||||||
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,
|
|
||||||
principalSchema: "core",
|
|
||||||
principalTable: "HangfireJob",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "HangfireQueuedJob",
|
|
||||||
schema: "core",
|
|
||||||
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,
|
|
||||||
principalSchema: "core",
|
|
||||||
principalTable: "HangfireJob",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "HangfireState",
|
|
||||||
schema: "core",
|
|
||||||
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,
|
|
||||||
principalSchema: "core",
|
|
||||||
principalTable: "HangfireJob",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireCounter_ExpireAt",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireCounter",
|
|
||||||
column: "ExpireAt");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireCounter_Key_Value",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireCounter",
|
|
||||||
columns: new[] { "Key", "Value" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireHash_ExpireAt",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireHash",
|
|
||||||
column: "ExpireAt");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireJob_ExpireAt",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireJob",
|
|
||||||
column: "ExpireAt");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireJob_StateId",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireJob",
|
|
||||||
column: "StateId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireJob_StateName",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireJob",
|
|
||||||
column: "StateName");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireList_ExpireAt",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireList",
|
|
||||||
column: "ExpireAt");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireQueuedJob_JobId",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireQueuedJob",
|
|
||||||
column: "JobId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireQueuedJob_Queue_FetchedAt",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireQueuedJob",
|
|
||||||
columns: new[] { "Queue", "FetchedAt" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireServer_Heartbeat",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireServer",
|
|
||||||
column: "Heartbeat");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireSet_ExpireAt",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireSet",
|
|
||||||
column: "ExpireAt");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireSet_Key_Score",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireSet",
|
|
||||||
columns: new[] { "Key", "Score" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_HangfireState_JobId",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireState",
|
|
||||||
column: "JobId");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_HangfireJob_HangfireState_StateId",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireJob",
|
|
||||||
column: "StateId",
|
|
||||||
principalSchema: "core",
|
|
||||||
principalTable: "HangfireState",
|
|
||||||
principalColumn: "Id");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_HangfireJob_HangfireState_StateId",
|
|
||||||
schema: "core",
|
|
||||||
table: "HangfireJob");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ApiKeys",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireCounter",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireHash",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireJobParameter",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireList",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireLock",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireQueuedJob",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireServer",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireSet",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Themes",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Users",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireState",
|
|
||||||
schema: "core");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "HangfireJob",
|
|
||||||
schema: "core");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,562 +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
|
|
||||||
.HasDefaultSchema("core")
|
|
||||||
.HasAnnotation("ProductVersion", "9.0.8")
|
|
||||||
.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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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("ApiKeys", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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("Themes", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
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("Users", "core");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("StateId");
|
|
||||||
|
|
||||||
b.Navigation("State");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
|
|
||||||
.WithMany("Parameters")
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
|
|
||||||
.WithMany("QueuedJobs")
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
|
|
||||||
.WithMany("States")
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
|
|
||||||
{
|
|
||||||
b.OwnsOne("Moonlight.ApiServer.Models.ApplicationTheme", "Content", b1 =>
|
|
||||||
{
|
|
||||||
b1.Property<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("Themes", "core");
|
|
||||||
|
|
||||||
b1.ToJson("Content");
|
|
||||||
|
|
||||||
b1.WithOwner()
|
|
||||||
.HasForeignKey("ThemeId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.Navigation("Content")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Parameters");
|
|
||||||
|
|
||||||
b.Navigation("QueuedJobs");
|
|
||||||
|
|
||||||
b.Navigation("States");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using System.IO.Compression;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Extensions;
|
|
||||||
|
|
||||||
public static class ZipArchiveExtensions
|
|
||||||
{
|
|
||||||
public static async Task AddBinaryAsync(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 AddTextAsync(this ZipArchive archive, string name, string content)
|
|
||||||
{
|
|
||||||
var data = Encoding.UTF8.GetBytes(content);
|
|
||||||
await archive.AddBinaryAsync(name, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task AddFileAsync(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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace Moonlight.ApiServer.Helpers;
|
|
||||||
|
|
||||||
public class FilePathHelper
|
|
||||||
{
|
|
||||||
public static 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using MoonCore.Common;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.ApiServer.Mappers;
|
|
||||||
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<ActionResult<CountedData<ApiKeyResponse>>> GetAsync(
|
|
||||||
[FromQuery] int startIndex,
|
|
||||||
[FromQuery] int count,
|
|
||||||
[FromQuery] string? orderBy,
|
|
||||||
[FromQuery] string? filter,
|
|
||||||
[FromQuery] string orderByDir = "asc"
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (count > 100)
|
|
||||||
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
|
|
||||||
|
|
||||||
IQueryable<ApiKey> query = ApiKeyRepository.Get();
|
|
||||||
|
|
||||||
query = orderBy switch
|
|
||||||
{
|
|
||||||
nameof(ApiKey.Id) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.Id)
|
|
||||||
: query.OrderBy(x => x.Id),
|
|
||||||
|
|
||||||
nameof(ApiKey.ExpiresAt) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.ExpiresAt)
|
|
||||||
: query.OrderBy(x => x.ExpiresAt),
|
|
||||||
|
|
||||||
nameof(ApiKey.CreatedAt) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.CreatedAt)
|
|
||||||
: query.OrderBy(x => x.CreatedAt),
|
|
||||||
|
|
||||||
_ => query.OrderBy(x => x.Id)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(filter))
|
|
||||||
{
|
|
||||||
query = query.Where(x =>
|
|
||||||
EF.Functions.ILike(x.Description, $"%{filter}%")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCount = await query.CountAsync();
|
|
||||||
|
|
||||||
var items = await query
|
|
||||||
.Skip(startIndex)
|
|
||||||
.Take(count)
|
|
||||||
.AsNoTracking()
|
|
||||||
.ProjectToResponse()
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
return new CountedData<ApiKeyResponse>()
|
|
||||||
{
|
|
||||||
Items = items,
|
|
||||||
TotalCount = totalCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.get")]
|
|
||||||
public async Task<ActionResult<ApiKeyResponse>> GetSingleAsync(int id)
|
|
||||||
{
|
|
||||||
var apiKey = await ApiKeyRepository
|
|
||||||
.Get()
|
|
||||||
.AsNoTracking()
|
|
||||||
.ProjectToResponse()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
|
|
||||||
if (apiKey == null)
|
|
||||||
return Problem("No api key with that id found", statusCode: 404);
|
|
||||||
|
|
||||||
return apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.create")]
|
|
||||||
public async Task<CreateApiKeyResponse> CreateAsync([FromBody] CreateApiKeyRequest request)
|
|
||||||
{
|
|
||||||
var apiKey = ApiKeyMapper.ToApiKey(request);
|
|
||||||
|
|
||||||
var finalApiKey = await ApiKeyRepository.AddAsync(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:int}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.update")]
|
|
||||||
public async Task<ActionResult<ApiKeyResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateApiKeyRequest request)
|
|
||||||
{
|
|
||||||
var apiKey = await ApiKeyRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
|
|
||||||
if (apiKey == null)
|
|
||||||
return Problem("No api key with that id found", statusCode: 404);
|
|
||||||
|
|
||||||
ApiKeyMapper.Merge(apiKey, request);
|
|
||||||
|
|
||||||
await ApiKeyRepository.UpdateAsync(apiKey);
|
|
||||||
|
|
||||||
return ApiKeyMapper.ToResponse(apiKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.apikeys.delete")]
|
|
||||||
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
|
||||||
{
|
|
||||||
var apiKey = await ApiKeyRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
|
|
||||||
if (apiKey == null)
|
|
||||||
return Problem("No api key with that id found", statusCode: 404);
|
|
||||||
|
|
||||||
await ApiKeyRepository.RemoveAsync(apiKey);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 FrontendAsync()
|
|
||||||
{
|
|
||||||
var stream = await FrontendService.GenerateZipAsync();
|
|
||||||
await Results.File(stream, fileDownloadName: "frontend.zip").ExecuteAsync(HttpContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using MoonCore.Common;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using 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<ActionResult<CountedData<ThemeResponse>>> GetAsync(
|
|
||||||
[FromQuery] int startIndex,
|
|
||||||
[FromQuery] int count,
|
|
||||||
[FromQuery] string? orderBy,
|
|
||||||
[FromQuery] string? filter,
|
|
||||||
[FromQuery] string orderByDir = "asc"
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (count > 100)
|
|
||||||
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
|
|
||||||
|
|
||||||
IQueryable<Theme> query = ThemeRepository.Get();
|
|
||||||
|
|
||||||
query = orderBy switch
|
|
||||||
{
|
|
||||||
nameof(Theme.Id) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.Id)
|
|
||||||
: query.OrderBy(x => x.Id),
|
|
||||||
|
|
||||||
nameof(Theme.Name) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.Name)
|
|
||||||
: query.OrderBy(x => x.Name),
|
|
||||||
|
|
||||||
nameof(Theme.Version) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.Version)
|
|
||||||
: query.OrderBy(x => x.Version),
|
|
||||||
|
|
||||||
_ => query.OrderBy(x => x.Id)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(filter))
|
|
||||||
{
|
|
||||||
query = query.Where(x =>
|
|
||||||
EF.Functions.ILike(x.Name, $"%{filter}%")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCount = await query.CountAsync();
|
|
||||||
|
|
||||||
var items = await query
|
|
||||||
.Skip(startIndex)
|
|
||||||
.Take(count)
|
|
||||||
.AsNoTracking()
|
|
||||||
.ProjectToResponse()
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
return new CountedData<ThemeResponse>()
|
|
||||||
{
|
|
||||||
Items = items,
|
|
||||||
TotalCount = totalCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
|
|
||||||
public async Task<ActionResult<ThemeResponse>> GetSingleAsync([FromRoute] int id)
|
|
||||||
{
|
|
||||||
var theme = await ThemeRepository
|
|
||||||
.Get()
|
|
||||||
.AsNoTracking()
|
|
||||||
.ProjectToResponse()
|
|
||||||
.FirstOrDefaultAsync(t => t.Id == id);
|
|
||||||
|
|
||||||
if (theme == null)
|
|
||||||
return Problem("Theme with this id not found", statusCode: 404);
|
|
||||||
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
|
||||||
public async Task<ActionResult<ThemeResponse>> CreateAsync([FromBody] CreateThemeRequest request)
|
|
||||||
{
|
|
||||||
var theme = ThemeMapper.ToTheme(request);
|
|
||||||
|
|
||||||
var finalTheme = await ThemeRepository.AddAsync(theme);
|
|
||||||
|
|
||||||
return ThemeMapper.ToResponse(finalTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
|
||||||
public async Task<ActionResult<ThemeResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateThemeRequest request)
|
|
||||||
{
|
|
||||||
var theme = await ThemeRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(t => t.Id == id);
|
|
||||||
|
|
||||||
if (theme == null)
|
|
||||||
return Problem("Theme with this id not found", statusCode: 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.RunTransactionAsync(set => { set.UpdateRange(otherThemes); });
|
|
||||||
}
|
|
||||||
|
|
||||||
ThemeMapper.Merge(theme, request);
|
|
||||||
|
|
||||||
await ThemeRepository.UpdateAsync(theme);
|
|
||||||
|
|
||||||
return ThemeMapper.ToResponse(theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
|
|
||||||
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
|
||||||
{
|
|
||||||
var theme = await ThemeRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
|
|
||||||
if (theme == null)
|
|
||||||
return Problem("Theme with this id not found", statusCode: 404);
|
|
||||||
|
|
||||||
await ThemeRepository.RemoveAsync(theme);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
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<ActionResult> DiagnoseAsync([FromBody] GenerateDiagnoseRequest request)
|
|
||||||
{
|
|
||||||
var stream = await DiagnoseService.GenerateDiagnoseAsync(request.Providers);
|
|
||||||
|
|
||||||
return File(stream, "application/zip", "diagnose.zip");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("providers")]
|
|
||||||
public async Task<ActionResult<DiagnoseProvideResponse[]>> GetProvidersAsync()
|
|
||||||
{
|
|
||||||
return await DiagnoseService.GetProvidersAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Helpers;
|
|
||||||
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/system/files")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.files")]
|
|
||||||
public class CombineController : Controller
|
|
||||||
{
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
|
|
||||||
private const string BaseDirectory = "storage";
|
|
||||||
|
|
||||||
public CombineController(AppConfiguration configuration)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("combine")]
|
|
||||||
public async Task<IResult> CombineAsync([FromBody] CombineRequest request)
|
|
||||||
{
|
|
||||||
// Validate file lenght
|
|
||||||
if (request.Files.Length < 2)
|
|
||||||
return Results.Problem("At least two files are required", statusCode: 400);
|
|
||||||
|
|
||||||
// Resolve the physical paths
|
|
||||||
var destination = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Destination));
|
|
||||||
|
|
||||||
var files = request.Files
|
|
||||||
.Select(path => Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(path)))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// Validate max file size
|
|
||||||
long combinedSize = 0;
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
var fi = new FileInfo(file);
|
|
||||||
combinedSize += fi.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ByteConverter.FromBytes(combinedSize).MegaBytes > Configuration.Files.CombineLimit)
|
|
||||||
{
|
|
||||||
return Results.Problem("The combine operation exceeds the maximum file size", statusCode: 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine files
|
|
||||||
|
|
||||||
await using var destinationFs = System.IO.File.Open(
|
|
||||||
destination,
|
|
||||||
FileMode.Create,
|
|
||||||
FileAccess.ReadWrite,
|
|
||||||
FileShare.Read
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
await using var fs = System.IO.File.Open(
|
|
||||||
file,
|
|
||||||
FileMode.Open,
|
|
||||||
FileAccess.ReadWrite
|
|
||||||
);
|
|
||||||
|
|
||||||
await fs.CopyToAsync(destinationFs);
|
|
||||||
await destinationFs.FlushAsync();
|
|
||||||
|
|
||||||
fs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
await destinationFs.FlushAsync();
|
|
||||||
destinationFs.Close();
|
|
||||||
|
|
||||||
return Results.Ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using ICSharpCode.SharpZipLib.GZip;
|
|
||||||
using ICSharpCode.SharpZipLib.Tar;
|
|
||||||
using ICSharpCode.SharpZipLib.Zip;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Helpers;
|
|
||||||
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/system/files")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.files")]
|
|
||||||
public class CompressController : Controller
|
|
||||||
{
|
|
||||||
private const string BaseDirectory = "storage";
|
|
||||||
|
|
||||||
[HttpPost("compress")]
|
|
||||||
public async Task<IResult> CompressAsync([FromBody] CompressRequest request)
|
|
||||||
{
|
|
||||||
// Validate item length
|
|
||||||
if (request.Items.Length == 0)
|
|
||||||
{
|
|
||||||
return Results.Problem(
|
|
||||||
"At least one item is required",
|
|
||||||
statusCode: 400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build paths
|
|
||||||
var destinationPath = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Destination));
|
|
||||||
var rootPath = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Root));
|
|
||||||
|
|
||||||
// Resolve the relative to the root item paths to absolute paths
|
|
||||||
var itemsPaths = request.Items.Select(item =>
|
|
||||||
Path.Combine(
|
|
||||||
BaseDirectory,
|
|
||||||
FilePathHelper.SanitizePath(
|
|
||||||
UnixPath.Combine(request.Root, item)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute request
|
|
||||||
switch (request.Format)
|
|
||||||
{
|
|
||||||
case "tar.gz":
|
|
||||||
await CompressTarGzAsync(destinationPath, itemsPaths, rootPath);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "zip":
|
|
||||||
await CompressZipAsync(destinationPath, itemsPaths, rootPath);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return Results.Problem("Unsupported archive format specified", statusCode: 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Results.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region Tar Gz
|
|
||||||
|
|
||||||
private async Task CompressTarGzAsync(string destination, IEnumerable<string> items, string root)
|
|
||||||
{
|
|
||||||
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 item in items)
|
|
||||||
await CompressItemToTarGzAsync(tarStream, item, root);
|
|
||||||
|
|
||||||
await tarStream.FlushAsync();
|
|
||||||
await gzoStream.FlushAsync();
|
|
||||||
await outStream.FlushAsync();
|
|
||||||
|
|
||||||
tarStream.Close();
|
|
||||||
gzoStream.Close();
|
|
||||||
outStream.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CompressItemToTarGzAsync(TarOutputStream tarOutputStream, string item, string root)
|
|
||||||
{
|
|
||||||
if (System.IO.File.Exists(item))
|
|
||||||
{
|
|
||||||
// Open file stream
|
|
||||||
var fs = System.IO.File.Open(item, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
|
|
||||||
// Meta
|
|
||||||
var entry = TarEntry.CreateTarEntry(
|
|
||||||
Formatter
|
|
||||||
.ReplaceStart(item, root, "")
|
|
||||||
.TrimStart('/')
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set size
|
|
||||||
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();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(item))
|
|
||||||
{
|
|
||||||
foreach (var fsEntry in Directory.EnumerateFileSystemEntries(item))
|
|
||||||
await CompressItemToTarGzAsync(tarOutputStream, fsEntry, root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ZIP
|
|
||||||
|
|
||||||
private async Task CompressZipAsync(string destination, IEnumerable<string> items, string root)
|
|
||||||
{
|
|
||||||
await using var outStream = System.IO.File.Create(destination);
|
|
||||||
await using var zipOutputStream = new ZipOutputStream(outStream);
|
|
||||||
|
|
||||||
foreach (var item in items)
|
|
||||||
await AddItemToZipAsync(zipOutputStream, item, root);
|
|
||||||
|
|
||||||
await zipOutputStream.FlushAsync();
|
|
||||||
await outStream.FlushAsync();
|
|
||||||
|
|
||||||
zipOutputStream.Close();
|
|
||||||
outStream.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddItemToZipAsync(ZipOutputStream outputStream, string item, string root)
|
|
||||||
{
|
|
||||||
if (System.IO.File.Exists(item))
|
|
||||||
{
|
|
||||||
// Open file stream
|
|
||||||
var fs = System.IO.File.Open(item, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
|
|
||||||
// Meta
|
|
||||||
var entry = new ZipEntry(
|
|
||||||
Formatter
|
|
||||||
.ReplaceStart(item, root, "")
|
|
||||||
.TrimStart('/')
|
|
||||||
);
|
|
||||||
|
|
||||||
entry.Size = fs.Length;
|
|
||||||
|
|
||||||
// Write entry
|
|
||||||
await outputStream.PutNextEntryAsync(entry, CancellationToken.None);
|
|
||||||
|
|
||||||
// Copy file content to tar stream
|
|
||||||
await fs.CopyToAsync(outputStream);
|
|
||||||
fs.Close();
|
|
||||||
|
|
||||||
// Close the entry
|
|
||||||
outputStream.CloseEntry();
|
|
||||||
|
|
||||||
// Flush caches
|
|
||||||
await outputStream.FlushAsync();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(item))
|
|
||||||
{
|
|
||||||
foreach (var subItem in Directory.EnumerateFileSystemEntries(item))
|
|
||||||
await AddItemToZipAsync(outputStream, subItem, root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,113 +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 Moonlight.ApiServer.Helpers;
|
|
||||||
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/system/files")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.files")]
|
|
||||||
public class DecompressController : Controller
|
|
||||||
{
|
|
||||||
private const string BaseDirectory = "storage";
|
|
||||||
|
|
||||||
[HttpPost("decompress")]
|
|
||||||
public async Task DecompressAsync([FromBody] DecompressRequest request)
|
|
||||||
{
|
|
||||||
var path = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Path));
|
|
||||||
var destination = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Destination));
|
|
||||||
|
|
||||||
switch (request.Format)
|
|
||||||
{
|
|
||||||
case "tar.gz":
|
|
||||||
await DecompressTarGzAsync(path, destination);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "zip":
|
|
||||||
await DecompressZipAsync(path, destination);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Tar Gz
|
|
||||||
|
|
||||||
private async Task DecompressTarGzAsync(string path, string destination)
|
|
||||||
{
|
|
||||||
await using var fs = System.IO.File.Open(path, 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 = FilePathHelper.SanitizePath(entry.Name);
|
|
||||||
var fileDestination = Path.Combine(destination, 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 DecompressZipAsync(string path, string destination)
|
|
||||||
{
|
|
||||||
await using var fs = System.IO.File.Open(path, 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 = FilePathHelper.SanitizePath(entry.Name);
|
|
||||||
var fileDestination = Path.Combine(destination, 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
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
using ICSharpCode.SharpZipLib.Zip;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Helpers;
|
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/system/files/downloadUrl")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.files")]
|
|
||||||
public class DownloadUrlController : Controller
|
|
||||||
{
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
|
|
||||||
private const string BaseDirectory = "storage";
|
|
||||||
|
|
||||||
public DownloadUrlController(AppConfiguration configuration)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task GetAsync([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var physicalPath = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(path));
|
|
||||||
var name = Path.GetFileName(physicalPath);
|
|
||||||
|
|
||||||
if (System.IO.File.Exists(physicalPath))
|
|
||||||
{
|
|
||||||
await using var fs = System.IO.File.Open(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
|
|
||||||
await Results.File(fs, fileDownloadName: name).ExecuteAsync(HttpContext);
|
|
||||||
}
|
|
||||||
else if(Directory.Exists(physicalPath))
|
|
||||||
{
|
|
||||||
// Without the base directory we would have the full path to the target folder
|
|
||||||
// inside the zip
|
|
||||||
|
|
||||||
var baseDirectory = Path.Combine(
|
|
||||||
BaseDirectory,
|
|
||||||
FilePathHelper.SanitizePath(Path.GetDirectoryName(path) ?? "")
|
|
||||||
);
|
|
||||||
|
|
||||||
Response.StatusCode = 200;
|
|
||||||
Response.ContentType = "application/zip";
|
|
||||||
Response.Headers["Content-Disposition"] = $"attachment; filename=\"{name}.zip\"";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var zipStream = new ZipOutputStream(Response.Body);
|
|
||||||
zipStream.IsStreamOwner = false;
|
|
||||||
|
|
||||||
await StreamFolderAsZipAsync(zipStream, physicalPath, baseDirectory, HttpContext.RequestAborted);
|
|
||||||
}
|
|
||||||
catch (ZipException)
|
|
||||||
{
|
|
||||||
// Ignored
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
// Ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StreamFolderAsZipAsync(
|
|
||||||
ZipOutputStream zipStream,
|
|
||||||
string path, string rootPath,
|
|
||||||
CancellationToken cancellationToken
|
|
||||||
)
|
|
||||||
{
|
|
||||||
foreach (var file in Directory.EnumerateFiles(path))
|
|
||||||
{
|
|
||||||
if (HttpContext.RequestAborted.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var fi = new FileInfo(file);
|
|
||||||
|
|
||||||
var filePath = Formatter.ReplaceStart(file, rootPath, "");
|
|
||||||
|
|
||||||
await zipStream.PutNextEntryAsync(new ZipEntry(filePath)
|
|
||||||
{
|
|
||||||
Size = fi.Length,
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
await using var fs = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
await fs.CopyToAsync(zipStream, cancellationToken);
|
|
||||||
await fs.FlushAsync(cancellationToken);
|
|
||||||
|
|
||||||
fs.Close();
|
|
||||||
|
|
||||||
await zipStream.FlushAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var directory in Directory.EnumerateDirectories(path))
|
|
||||||
{
|
|
||||||
if (HttpContext.RequestAborted.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await StreamFolderAsZipAsync(zipStream, directory, rootPath, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Yes I know we can just create that url on the client as the exist validation is done on both endpoints,
|
|
||||||
// but we leave it here for future modifications. E.g. using a distributed file provider or smth like that
|
|
||||||
[HttpPost]
|
|
||||||
public Task<DownloadUrlResponse> PostAsync([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = FilePathHelper.SanitizePath(path);
|
|
||||||
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
if (System.IO.File.Exists(physicalPath) || Directory.Exists(physicalPath))
|
|
||||||
{
|
|
||||||
return Task.FromResult(new DownloadUrlResponse()
|
|
||||||
{
|
|
||||||
Url = $"{Configuration.PublicUrl}/api/admin/system/files/downloadUrl?path={path}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpApiException("No such file or directory found", 404);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using Moonlight.ApiServer.Helpers;
|
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/system/files")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.files")]
|
|
||||||
public class FilesController : Controller
|
|
||||||
{
|
|
||||||
private const string BaseDirectory = "storage";
|
|
||||||
|
|
||||||
[HttpPost("touch")]
|
|
||||||
public async Task CreateFileAsync([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = FilePathHelper.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 CreateFolderAsync([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = FilePathHelper.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[]> ListAsync([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = FilePathHelper.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("move")]
|
|
||||||
public Task MoveAsync([FromQuery] string oldPath, [FromQuery] string newPath)
|
|
||||||
{
|
|
||||||
var oldSafePath = FilePathHelper.SanitizePath(oldPath);
|
|
||||||
var newSafePath = FilePathHelper.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 DeleteAsync([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = FilePathHelper.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("upload")]
|
|
||||||
public async Task<IResult> UploadAsync([FromQuery] string path)
|
|
||||||
{
|
|
||||||
if (Request.Form.Files.Count != 1)
|
|
||||||
return Results.Problem("Only one file is allowed in the request", statusCode: 400);
|
|
||||||
|
|
||||||
var file = Request.Form.Files[0];
|
|
||||||
|
|
||||||
var safePath = FilePathHelper.SanitizePath(path);
|
|
||||||
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
// Create directory which the new file should be put into
|
|
||||||
var baseDirectory = Path.GetDirectoryName(physicalPath);
|
|
||||||
|
|
||||||
if(!string.IsNullOrEmpty(baseDirectory))
|
|
||||||
Directory.CreateDirectory(baseDirectory);
|
|
||||||
|
|
||||||
// Create file from provided form
|
|
||||||
await using var dataStream = file.OpenReadStream();
|
|
||||||
|
|
||||||
await using var targetStream = System.IO.File.Open(
|
|
||||||
physicalPath,
|
|
||||||
FileMode.Create,
|
|
||||||
FileAccess.ReadWrite,
|
|
||||||
FileShare.Read
|
|
||||||
);
|
|
||||||
|
|
||||||
// Copy the content to the newly created file
|
|
||||||
await dataStream.CopyToAsync(targetStream);
|
|
||||||
await targetStream.FlushAsync();
|
|
||||||
|
|
||||||
// Close both streams
|
|
||||||
targetStream.Close();
|
|
||||||
dataStream.Close();
|
|
||||||
|
|
||||||
return Results.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("download")]
|
|
||||||
public async Task DownloadAsync([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = FilePathHelper.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using Hangfire;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Hangfire;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/system/hangfire")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.hangfire")]
|
|
||||||
public class HangfireController : Controller
|
|
||||||
{
|
|
||||||
private readonly JobStorage JobStorage;
|
|
||||||
|
|
||||||
public HangfireController(JobStorage jobStorage)
|
|
||||||
{
|
|
||||||
JobStorage = jobStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("stats")]
|
|
||||||
public Task<HangfireStatsResponse> GetStatsAsync()
|
|
||||||
{
|
|
||||||
var statistics = JobStorage.GetMonitoringApi().GetStatistics();
|
|
||||||
|
|
||||||
return Task.FromResult(new HangfireStatsResponse()
|
|
||||||
{
|
|
||||||
Awaiting = statistics.Awaiting,
|
|
||||||
Deleted = statistics.Deleted,
|
|
||||||
Enqueued = statistics.Enqueued,
|
|
||||||
Failed = statistics.Failed,
|
|
||||||
Processing = statistics.Processing,
|
|
||||||
Queues = statistics.Queues,
|
|
||||||
Recurring = statistics.Recurring,
|
|
||||||
Retries = statistics.Retries,
|
|
||||||
Scheduled = statistics.Scheduled,
|
|
||||||
Servers = statistics.Servers,
|
|
||||||
Succeeded = statistics.Succeeded
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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> GetOverviewAsync()
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Uptime = await ApplicationService.GetUptimeAsync(),
|
|
||||||
CpuUsage = await ApplicationService.GetCpuUsageAsync(),
|
|
||||||
MemoryUsage = await ApplicationService.GetMemoryUsageAsync(),
|
|
||||||
OperatingSystem = await ApplicationService.GetOsNameAsync()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("shutdown")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.shutdown")]
|
|
||||||
public async Task ShutdownAsync()
|
|
||||||
{
|
|
||||||
await ApplicationService.ShutdownAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using MoonCore.Common;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using MoonCore.Extended.Helpers;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.ApiServer.Services;
|
|
||||||
using Moonlight.ApiServer.Mappers;
|
|
||||||
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<ActionResult<CountedData<UserResponse>>> GetAsync(
|
|
||||||
[FromQuery] int startIndex,
|
|
||||||
[FromQuery] int count,
|
|
||||||
[FromQuery] string? orderBy,
|
|
||||||
[FromQuery] string? filter,
|
|
||||||
[FromQuery] string orderByDir = "asc"
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (count > 100)
|
|
||||||
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
|
|
||||||
|
|
||||||
IQueryable<User> query = UserRepository.Get();
|
|
||||||
|
|
||||||
query = orderBy switch
|
|
||||||
{
|
|
||||||
nameof(Database.Entities.User.Id) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.Id)
|
|
||||||
: query.OrderBy(x => x.Id),
|
|
||||||
|
|
||||||
nameof(Database.Entities.User.Username) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.Username)
|
|
||||||
: query.OrderBy(x => x.Username),
|
|
||||||
|
|
||||||
nameof(Database.Entities.User.Email) => orderByDir == "desc"
|
|
||||||
? query.OrderByDescending(x => x.Email)
|
|
||||||
: query.OrderBy(x => x.Email),
|
|
||||||
|
|
||||||
_ => query.OrderBy(x => x.Id)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(filter))
|
|
||||||
{
|
|
||||||
query = query.Where(x =>
|
|
||||||
EF.Functions.ILike(x.Username, $"%{filter}%") ||
|
|
||||||
EF.Functions.ILike(x.Email, $"%{filter}%")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCount = await query.CountAsync();
|
|
||||||
|
|
||||||
var items = await query
|
|
||||||
.Skip(startIndex)
|
|
||||||
.Take(count)
|
|
||||||
.AsNoTracking()
|
|
||||||
.ProjectToResponse()
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
return new CountedData<UserResponse>()
|
|
||||||
{
|
|
||||||
Items = items,
|
|
||||||
TotalCount = totalCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.users.get")]
|
|
||||||
public async Task<ActionResult<UserResponse>> GetSingleAsync(int id)
|
|
||||||
{
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.ProjectToResponse()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return Problem("No user with that id found", statusCode: 404);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize(Policy = "permissions:admin.users.create")]
|
|
||||||
public async Task<ActionResult<UserResponse>> CreateAsync([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))
|
|
||||||
return Problem("A user with that username already exists", statusCode: 400);
|
|
||||||
|
|
||||||
if (UserRepository.Get().Any(x => x.Email == request.Email))
|
|
||||||
return Problem("A user with that email address already exists", statusCode: 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.AddAsync(user);
|
|
||||||
|
|
||||||
return UserMapper.ToResponse(finalUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("{id}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.users.update")]
|
|
||||||
public async Task<ActionResult<UserResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateUserRequest request)
|
|
||||||
{
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return Problem("No user with that id found", statusCode: 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))
|
|
||||||
return Problem("Another user with that username already exists", statusCode: 400);
|
|
||||||
|
|
||||||
if (UserRepository.Get().Any(x => x.Email == request.Email && x.Id != user.Id))
|
|
||||||
return Problem("Another user with that email address already exists", statusCode: 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.UpdateAsync(user);
|
|
||||||
|
|
||||||
return UserMapper.ToResponse(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
[Authorize(Policy = "permissions:admin.users.delete")]
|
|
||||||
public async Task<ActionResult> DeleteAsync([FromRoute] int id, [FromQuery] bool force = false)
|
|
||||||
{
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return Problem("No user with that id found", statusCode: 404);
|
|
||||||
|
|
||||||
var deletionService = HttpContext.RequestServices.GetRequiredService<UserDeletionService>();
|
|
||||||
|
|
||||||
if (!force)
|
|
||||||
{
|
|
||||||
var validationResult = await deletionService.ValidateAsync(user);
|
|
||||||
|
|
||||||
if (!validationResult.IsAllowed)
|
|
||||||
return Problem("Unable to delete user", statusCode: 400, title: validationResult.Reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
await deletionService.DeleteAsync(user, force);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Interfaces;
|
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/auth")]
|
|
||||||
public class AuthController : Controller
|
|
||||||
{
|
|
||||||
private readonly IAuthenticationSchemeProvider SchemeProvider;
|
|
||||||
private readonly IEnumerable<IAuthCheckExtension> Extensions;
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
|
|
||||||
public AuthController(
|
|
||||||
IAuthenticationSchemeProvider schemeProvider,
|
|
||||||
IEnumerable<IAuthCheckExtension> extensions,
|
|
||||||
AppConfiguration configuration
|
|
||||||
)
|
|
||||||
{
|
|
||||||
SchemeProvider = schemeProvider;
|
|
||||||
Extensions = extensions;
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<AuthSchemeResponse[]> GetSchemesAsync()
|
|
||||||
{
|
|
||||||
var schemes = await SchemeProvider.GetAllSchemesAsync();
|
|
||||||
|
|
||||||
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
|
|
||||||
|
|
||||||
return schemes
|
|
||||||
.Where(x => allowedSchemes.Contains(x.Name))
|
|
||||||
.Select(scheme => new AuthSchemeResponse()
|
|
||||||
{
|
|
||||||
DisplayName = scheme.DisplayName ?? scheme.Name,
|
|
||||||
Identifier = scheme.Name
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{identifier:alpha}")]
|
|
||||||
public async Task StartSchemeAsync([FromRoute] string identifier)
|
|
||||||
{
|
|
||||||
// Validate identifier against our enable list
|
|
||||||
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
|
|
||||||
|
|
||||||
if (!allowedSchemes.Contains(identifier))
|
|
||||||
{
|
|
||||||
await Results
|
|
||||||
.Problem(
|
|
||||||
"Invalid scheme identifier provided",
|
|
||||||
statusCode: 404
|
|
||||||
)
|
|
||||||
.ExecuteAsync(HttpContext);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we can check if it even exists
|
|
||||||
var scheme = await SchemeProvider.GetSchemeAsync(identifier);
|
|
||||||
|
|
||||||
if (scheme == null)
|
|
||||||
{
|
|
||||||
await Results
|
|
||||||
.Problem(
|
|
||||||
"Invalid scheme identifier provided",
|
|
||||||
statusCode: 404
|
|
||||||
)
|
|
||||||
.ExecuteAsync(HttpContext);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything fine, challenge the frontend
|
|
||||||
await HttpContext.ChallengeAsync(
|
|
||||||
scheme.Name,
|
|
||||||
new AuthenticationProperties()
|
|
||||||
{
|
|
||||||
RedirectUri = "/"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpGet("check")]
|
|
||||||
public async Task<AuthClaimResponse[]> CheckAsync()
|
|
||||||
{
|
|
||||||
var username = User.FindFirstValue(ClaimTypes.Name)!;
|
|
||||||
var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
|
|
||||||
var email = User.FindFirstValue(ClaimTypes.Email)!;
|
|
||||||
var userId = User.FindFirstValue("UserId")!;
|
|
||||||
var permissions = User.FindFirstValue("Permissions")!;
|
|
||||||
|
|
||||||
// Create basic set of claims used by the frontend
|
|
||||||
var claims = new List<AuthClaimResponse>()
|
|
||||||
{
|
|
||||||
new(ClaimTypes.Name, username),
|
|
||||||
new(ClaimTypes.NameIdentifier, id),
|
|
||||||
new(ClaimTypes.Email, email),
|
|
||||||
new("UserId", userId),
|
|
||||||
new("Permissions", permissions)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enrich the frontend claims by extensions (used by plugins)
|
|
||||||
foreach (var extension in Extensions)
|
|
||||||
{
|
|
||||||
claims.AddRange(
|
|
||||||
await extension.GetFrontendClaimsAsync(User)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("logout")]
|
|
||||||
public async Task LogoutAsync()
|
|
||||||
{
|
|
||||||
await HttpContext.SignOutAsync();
|
|
||||||
await Results.Redirect("/").ExecuteAsync(HttpContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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> GetConfigurationAsync()
|
|
||||||
=> await FrontendService.GetConfigurationAsync();
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IResult> IndexAsync()
|
|
||||||
{
|
|
||||||
var content = await FrontendService.GenerateIndexHtmlAsync();
|
|
||||||
|
|
||||||
return Results.Text(content, "text/html", Encoding.UTF8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
@using Moonlight.ApiServer.Database.Entities
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="bg-base-200 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 {
|
|
||||||
--color-base-100: @(Theme.Content.ColorBase100);
|
|
||||||
--color-base-200: @(Theme.Content.ColorBase200);
|
|
||||||
--color-base-300: @(Theme.Content.ColorBase300);
|
|
||||||
--color-base-content: @(Theme.Content.ColorBaseContent);
|
|
||||||
|
|
||||||
--color-primary: @(Theme.Content.ColorPrimary);
|
|
||||||
--color-primary-content: @(Theme.Content.ColorPrimaryContent);
|
|
||||||
|
|
||||||
--color-secondary: @(Theme.Content.ColorSecondary);
|
|
||||||
--color-secondary-content: @(Theme.Content.ColorSecondaryContent);
|
|
||||||
|
|
||||||
--color-accent: @(Theme.Content.ColorAccent);
|
|
||||||
--color-accent-content: @(Theme.Content.ColorAccentContent);
|
|
||||||
|
|
||||||
--color-neutral: @(Theme.Content.ColorNeutral);
|
|
||||||
--color-neutral-content: @(Theme.Content.ColorNeutralContent);
|
|
||||||
|
|
||||||
--color-info: @(Theme.Content.ColorInfo);
|
|
||||||
--color-info-content: @(Theme.Content.ColorInfoContent);
|
|
||||||
|
|
||||||
--color-success: @(Theme.Content.ColorSuccess);
|
|
||||||
--color-success-content: @(Theme.Content.ColorSuccessContent);
|
|
||||||
|
|
||||||
--color-warning: @(Theme.Content.ColorWarning);
|
|
||||||
--color-warning-content: @(Theme.Content.ColorWarningContent);
|
|
||||||
|
|
||||||
--color-error: @(Theme.Content.ColorError);
|
|
||||||
--color-error-content: @(Theme.Content.ColorErrorContent);
|
|
||||||
|
|
||||||
--radius-selector: @(Theme.Content.RadiusSelector)rem;
|
|
||||||
--radius-field: @(Theme.Content.RadiusField)rem;
|
|
||||||
--radius-box: @(Theme.Content.RadiusBox)rem;
|
|
||||||
|
|
||||||
--size-selector: @(Theme.Content.SizeSelector)rem;
|
|
||||||
--size-field: @(Theme.Content.SizeField)rem;
|
|
||||||
|
|
||||||
--border: @(Theme.Content.Border)px;
|
|
||||||
--depth: @(Theme.Content.Depth);
|
|
||||||
--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; }
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using MoonCore.Extended.Helpers;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.ApiServer.Implementations.LocalAuth;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.LocalAuth;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/localAuth")]
|
|
||||||
public class LocalAuthController : Controller
|
|
||||||
{
|
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
|
||||||
private readonly IServiceProvider ServiceProvider;
|
|
||||||
private readonly IAuthenticationService AuthenticationService;
|
|
||||||
private readonly IOptionsMonitor<LocalAuthOptions> Options;
|
|
||||||
private readonly ILogger<LocalAuthController> Logger;
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
|
|
||||||
public LocalAuthController(
|
|
||||||
DatabaseRepository<User> userRepository,
|
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
IAuthenticationService authenticationService,
|
|
||||||
IOptionsMonitor<LocalAuthOptions> options,
|
|
||||||
ILogger<LocalAuthController> logger,
|
|
||||||
AppConfiguration configuration
|
|
||||||
)
|
|
||||||
{
|
|
||||||
UserRepository = userRepository;
|
|
||||||
ServiceProvider = serviceProvider;
|
|
||||||
AuthenticationService = authenticationService;
|
|
||||||
Options = options;
|
|
||||||
Logger = logger;
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[HttpGet("login")]
|
|
||||||
public async Task<ActionResult> LoginAsync()
|
|
||||||
{
|
|
||||||
var html = await ComponentHelper.RenderToHtmlAsync<Login>(ServiceProvider);
|
|
||||||
|
|
||||||
return Content(html, "text/html");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("register")]
|
|
||||||
public async Task<ActionResult> RegisterAsync()
|
|
||||||
{
|
|
||||||
var html = await ComponentHelper.RenderToHtmlAsync<Register>(ServiceProvider);
|
|
||||||
|
|
||||||
return Content(html, "text/html");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[HttpPost("login")]
|
|
||||||
public async Task<ActionResult> LoginAsync([FromForm] string email, [FromForm] string password)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Perform login
|
|
||||||
var user = await InternalLoginAsync(email, password);
|
|
||||||
|
|
||||||
// Login user
|
|
||||||
var options = Options.Get(LocalAuthConstants.AuthenticationScheme);
|
|
||||||
|
|
||||||
await AuthenticationService.SignInAsync(HttpContext, options.SignInScheme, new ClaimsPrincipal(
|
|
||||||
new ClaimsIdentity(
|
|
||||||
[
|
|
||||||
new Claim(ClaimTypes.Email, user.Email),
|
|
||||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
|
||||||
new Claim(ClaimTypes.Name, user.Username)
|
|
||||||
],
|
|
||||||
LocalAuthConstants.AuthenticationScheme
|
|
||||||
)
|
|
||||||
), new AuthenticationProperties());
|
|
||||||
|
|
||||||
// Redirect back to wasm app
|
|
||||||
return Redirect("/");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
string errorMessage;
|
|
||||||
|
|
||||||
if (e is AggregateException aggregateException)
|
|
||||||
errorMessage = aggregateException.Message;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
errorMessage = "An internal error occured";
|
|
||||||
Logger.LogError(e, "An unhandled error occured while logging in user");
|
|
||||||
}
|
|
||||||
|
|
||||||
var html = await ComponentHelper.RenderToHtmlAsync<Login>(ServiceProvider,
|
|
||||||
parameters => { parameters["ErrorMessage"] = errorMessage; });
|
|
||||||
|
|
||||||
return Content(html, "text/html");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("register")]
|
|
||||||
public async Task<ActionResult> RegisterAsync([FromForm] string email, [FromForm] string password, [FromForm] string username)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Perform register
|
|
||||||
var user = await InternalRegisterAsync(username, email, password);
|
|
||||||
|
|
||||||
// Login user
|
|
||||||
var options = Options.Get(LocalAuthConstants.AuthenticationScheme);
|
|
||||||
|
|
||||||
await AuthenticationService.SignInAsync(HttpContext, options.SignInScheme, new ClaimsPrincipal(
|
|
||||||
new ClaimsIdentity(
|
|
||||||
[
|
|
||||||
new Claim(ClaimTypes.Email, user.Email),
|
|
||||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
|
||||||
new Claim(ClaimTypes.Name, user.Username)
|
|
||||||
],
|
|
||||||
LocalAuthConstants.AuthenticationScheme
|
|
||||||
)
|
|
||||||
), new AuthenticationProperties());
|
|
||||||
|
|
||||||
// Redirect back to wasm app
|
|
||||||
return Redirect("/");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
string errorMessage;
|
|
||||||
|
|
||||||
if (e is AggregateException aggregateException)
|
|
||||||
errorMessage = aggregateException.Message;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
errorMessage = "An internal error occured";
|
|
||||||
Logger.LogError(e, "An unhandled error occured while logging in user");
|
|
||||||
}
|
|
||||||
|
|
||||||
var html = await ComponentHelper.RenderToHtmlAsync<Register>(ServiceProvider,
|
|
||||||
parameters => { parameters["ErrorMessage"] = errorMessage; });
|
|
||||||
|
|
||||||
return Content(html, "text/html");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<User> InternalRegisterAsync(string username, string email, string password)
|
|
||||||
{
|
|
||||||
email = email.ToLower();
|
|
||||||
username = username.ToLower();
|
|
||||||
|
|
||||||
if (await UserRepository.Get().AnyAsync(x => x.Username == username))
|
|
||||||
throw new AggregateException("A account with that username already exists");
|
|
||||||
|
|
||||||
if (await UserRepository.Get().AnyAsync(x => x.Email == email))
|
|
||||||
throw new AggregateException("A account with that email already exists");
|
|
||||||
|
|
||||||
string[] permissions = [];
|
|
||||||
|
|
||||||
if (Configuration.Authentication.FirstUserAdmin)
|
|
||||||
{
|
|
||||||
var count = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.CountAsync();
|
|
||||||
|
|
||||||
if (count == 0)
|
|
||||||
permissions = ["*"];
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = new User()
|
|
||||||
{
|
|
||||||
Username = username,
|
|
||||||
Email = email,
|
|
||||||
Password = HashHelper.Hash(password),
|
|
||||||
Permissions = permissions
|
|
||||||
};
|
|
||||||
|
|
||||||
var finalUser = await UserRepository.AddAsync(user);
|
|
||||||
|
|
||||||
return finalUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<User> InternalLoginAsync(string email, string password)
|
|
||||||
{
|
|
||||||
email = email.ToLower();
|
|
||||||
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Email == email);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw new AggregateException("Invalid combination of email and password");
|
|
||||||
|
|
||||||
if (!HashHelper.Verify(password, user.Password))
|
|
||||||
throw new AggregateException("Invalid combination of email and password");
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<html lang="en" class="h-full bg-base-200">
|
|
||||||
<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="/api/localAuth/register" class="link link-animated link-primary font-normal">Create an account</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
[Parameter] public string? ErrorMessage { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<html lang="en" class="h-full bg-base-200">
|
|
||||||
<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="/api/localAuth/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? ErrorMessage { get; set; }
|
|
||||||
}
|
|
||||||
@@ -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> GetAsync()
|
|
||||||
{
|
|
||||||
if (!Configuration.Development.EnableApiDocs)
|
|
||||||
return BadRequest("Api docs are disabled");
|
|
||||||
|
|
||||||
var options = new ApiDocsOptions();
|
|
||||||
var optionsJson = JsonSerializer.Serialize(options);
|
|
||||||
|
|
||||||
var html = await ComponentHelper.RenderToHtmlAsync<SwaggerPage>(
|
|
||||||
ServiceProvider,
|
|
||||||
parameters =>
|
|
||||||
{
|
|
||||||
parameters.Add("Options", optionsJson);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return Content(html, "text/html");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Hubs;
|
|
||||||
|
|
||||||
[Authorize(Policy = "permissions:admin.system.diagnose")]
|
|
||||||
public class DiagnoseHub : Hub
|
|
||||||
{
|
|
||||||
[HubMethodName("Ping")]
|
|
||||||
public async Task PingAsync()
|
|
||||||
{
|
|
||||||
await Clients.All.SendAsync("Pong");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Moonlight.ApiServer;
|
|
||||||
|
|
||||||
public interface IAssemblyMarker;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using MoonCore.Yaml;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Extensions;
|
|
||||||
using Moonlight.ApiServer.Interfaces;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations.Diagnose;
|
|
||||||
|
|
||||||
public class CoreConfigDiagnoseProvider : IDiagnoseProvider
|
|
||||||
{
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
|
|
||||||
public CoreConfigDiagnoseProvider(AppConfiguration configuration)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CheckForNullOrEmpty(string? content)
|
|
||||||
{
|
|
||||||
return string.IsNullOrEmpty(content)
|
|
||||||
? "ISEMPTY"
|
|
||||||
: "ISNOTEMPTY";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ModifyZipArchiveAsync(ZipArchive archive)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var configString = YamlSerializer.Serialize(Configuration);
|
|
||||||
var configuration = YamlSerializer.Deserialize<AppConfiguration>(configString);
|
|
||||||
|
|
||||||
configuration.Database.Password = CheckForNullOrEmpty(configuration.Database.Password);
|
|
||||||
configuration.Authentication.Secret = CheckForNullOrEmpty(configuration.Authentication.Secret);
|
|
||||||
configuration.SignalR.RedisConnectionString = CheckForNullOrEmpty(configuration.SignalR.RedisConnectionString);
|
|
||||||
|
|
||||||
await archive.AddTextAsync(
|
|
||||||
"core/config.txt",
|
|
||||||
YamlSerializer.Serialize(configuration)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
await archive.AddTextAsync("core/config.txt", $"Unable to load config: {e.ToStringDemystified()}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +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 ModifyZipArchiveAsync(ZipArchive archive)
|
|
||||||
{
|
|
||||||
var path = Path.Combine("storage", "logs", "moonlight.log");
|
|
||||||
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
var logsContent = await File.ReadAllTextAsync(path);
|
|
||||||
await archive.AddTextAsync("logs.txt", logsContent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
await archive.AddTextAsync("logs.txt", "Logs file moonlight.log has not been found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Moonlight.ApiServer.Implementations.LocalAuth;
|
|
||||||
|
|
||||||
public static class LocalAuthConstants
|
|
||||||
{
|
|
||||||
public const string AuthenticationScheme = "LocalAuth";
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using System.Text.Encodings.Web;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations.LocalAuth;
|
|
||||||
|
|
||||||
public class LocalAuthHandler : AuthenticationHandler<LocalAuthOptions>
|
|
||||||
{
|
|
||||||
public LocalAuthHandler(
|
|
||||||
IOptionsMonitor<LocalAuthOptions> options,
|
|
||||||
ILoggerFactory logger,
|
|
||||||
UrlEncoder encoder
|
|
||||||
) : base(options, logger, encoder)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
||||||
{
|
|
||||||
return Task.FromResult(
|
|
||||||
AuthenticateResult.Fail("Local authentication does not directly support AuthenticateAsync")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
|
|
||||||
{
|
|
||||||
await Results
|
|
||||||
.Redirect("/api/localAuth")
|
|
||||||
.ExecuteAsync(Context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations.LocalAuth;
|
|
||||||
|
|
||||||
public class LocalAuthOptions : AuthenticationSchemeOptions
|
|
||||||
{
|
|
||||||
public string? SignInScheme { get; set; }
|
|
||||||
}
|
|
||||||
@@ -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 InitializeAsync(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 RunAsync(IServiceProvider provider, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var applicationService = provider.GetRequiredService<ApplicationService>();
|
|
||||||
|
|
||||||
var memory = await applicationService.GetMemoryUsageAsync();
|
|
||||||
MemoryUsage.Record(memory);
|
|
||||||
|
|
||||||
var uptime = await applicationService.GetUptimeAsync();
|
|
||||||
Uptime.Record(uptime.TotalSeconds);
|
|
||||||
|
|
||||||
var cpu = await applicationService.GetCpuUsageAsync();
|
|
||||||
CpuUsage.Record(cpu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 InitializeAsync(Meter meter)
|
|
||||||
{
|
|
||||||
Users = meter.CreateGauge<int>("moonlight_users");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RunAsync(IServiceProvider provider, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var usersRepo = provider.GetRequiredService<DatabaseRepository<User>>();
|
|
||||||
var count = await usersRepo.Get().CountAsync(cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
Users.Record(count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
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.Logs;
|
|
||||||
using OpenTelemetry.Metrics;
|
|
||||||
using OpenTelemetry.Resources;
|
|
||||||
using OpenTelemetry.Trace;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations.Startup;
|
|
||||||
|
|
||||||
public class CoreStartup : IPluginStartup
|
|
||||||
{
|
|
||||||
public void AddPlugin(WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
builder.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
#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.OpenTelemetry.Enable)
|
|
||||||
{
|
|
||||||
var openTel = builder.Services.AddOpenTelemetry();
|
|
||||||
var openTelConfig = configuration.OpenTelemetry;
|
|
||||||
|
|
||||||
var resourceBuilder = ResourceBuilder.CreateDefault();
|
|
||||||
resourceBuilder.AddService(serviceName: "moonlight");
|
|
||||||
|
|
||||||
openTel.ConfigureResource(x => x.AddService(serviceName: "moonlight"));
|
|
||||||
|
|
||||||
if (openTelConfig.Metrics.Enable)
|
|
||||||
{
|
|
||||||
builder.Services.AddSingleton<MetricsBackgroundService>();
|
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<MetricsBackgroundService>());
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<IMetric, ApplicationMetric>();
|
|
||||||
builder.Services.AddSingleton<IMetric, UsersMetric>();
|
|
||||||
|
|
||||||
openTel.WithMetrics(providerBuilder =>
|
|
||||||
{
|
|
||||||
providerBuilder.AddAspNetCoreInstrumentation();
|
|
||||||
providerBuilder.AddOtlpExporter();
|
|
||||||
|
|
||||||
if (openTelConfig.Metrics.EnablePrometheus)
|
|
||||||
providerBuilder.AddPrometheusExporter();
|
|
||||||
|
|
||||||
providerBuilder.AddMeter("moonlight");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openTelConfig.Logs.Enable)
|
|
||||||
{
|
|
||||||
openTel.WithLogging();
|
|
||||||
|
|
||||||
builder.Logging.AddOpenTelemetry(options =>
|
|
||||||
{
|
|
||||||
options.SetResourceBuilder(resourceBuilder);
|
|
||||||
options.AddOtlpExporter();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openTelConfig.Traces.Enable)
|
|
||||||
{
|
|
||||||
openTel.WithTracing(providerBuilder =>
|
|
||||||
{
|
|
||||||
providerBuilder.AddAspNetCoreInstrumentation();
|
|
||||||
|
|
||||||
providerBuilder.AddOtlpExporter();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#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
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UsePlugin(WebApplication app)
|
|
||||||
{
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
app.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
#region Prometheus
|
|
||||||
|
|
||||||
if (configuration.OpenTelemetry is { Enable: true, Metrics.EnablePrometheus: true })
|
|
||||||
app.UseOpenTelemetryPrometheusScrapingEndpoint();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MapPlugin(WebApplication app)
|
|
||||||
{
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
app.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
if (configuration.Development.EnableApiDocs)
|
|
||||||
app.MapSwagger("/api/swagger/{documentName}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Interfaces;
|
|
||||||
|
|
||||||
public interface IAuthCheckExtension
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This function will be called by the frontend reaching out to the api server for claim information.
|
|
||||||
/// You can use this function to give your frontend plugins access to user specific data which is
|
|
||||||
/// static for the session. E.g. the avatar url of a user
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="principal">The principal of the current signed-in user</param>
|
|
||||||
/// <returns>An array of claim responses which gets added to the list of claims to send to the frontend</returns>
|
|
||||||
public Task<AuthClaimResponse[]> GetFrontendClaimsAsync(ClaimsPrincipal principal);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using System.IO.Compression;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Interfaces;
|
|
||||||
|
|
||||||
public interface IDiagnoseProvider
|
|
||||||
{
|
|
||||||
public Task ModifyZipArchiveAsync(ZipArchive archive);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using System.Diagnostics.Metrics;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Interfaces;
|
|
||||||
|
|
||||||
public interface IMetric
|
|
||||||
{
|
|
||||||
public Task InitializeAsync(Meter meter);
|
|
||||||
public Task RunAsync(IServiceProvider provider, CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Interfaces;
|
|
||||||
|
|
||||||
public interface IUserAuthExtension
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This function is called on every sign-in. It should be used to synchronize additional user data from the principal
|
|
||||||
/// or extend the claims saved in the user session
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="user">The current user this method is called for</param>
|
|
||||||
/// <param name="principal">The principal after being processed by moonlight itself</param>
|
|
||||||
/// <returns>The result of the synchronisation. Returning false will immediately invalidate the sign-in and no other extensions will be called</returns>
|
|
||||||
public Task<bool> SyncAsync(User user, ClaimsPrincipal principal);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IMPORTANT: Please note that heavy operations should not occur in this method as it will be called for every request
|
|
||||||
/// of every user
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="user">The current user this method is called for</param>
|
|
||||||
/// <param name="principal">The principal after being processed by moonlight itself</param>
|
|
||||||
/// <returns>The result of the validation. Returning false will immediately invalidate the users session and no other extensions will be called</returns>
|
|
||||||
public Task<bool> ValidateAsync(User user, ClaimsPrincipal principal);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.ApiServer.Models;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Interfaces;
|
|
||||||
|
|
||||||
public interface IUserDeleteHandler
|
|
||||||
{
|
|
||||||
public Task<UserDeleteValidationResult> ValidateAsync(User user);
|
|
||||||
public Task DeleteAsync(User user, bool force);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
|
||||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
|
||||||
using Riok.Mapperly.Abstractions;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Mappers;
|
|
||||||
|
|
||||||
[Mapper]
|
|
||||||
public static partial class ApiKeyMapper
|
|
||||||
{
|
|
||||||
// Mappers
|
|
||||||
public static partial ApiKeyResponse ToResponse(ApiKey apiKey);
|
|
||||||
public static partial ApiKey ToApiKey(CreateApiKeyRequest request);
|
|
||||||
public static partial void Merge([MappingTarget] ApiKey apiKey, UpdateApiKeyRequest request);
|
|
||||||
|
|
||||||
// EF Relations
|
|
||||||
|
|
||||||
public static partial IQueryable<ApiKeyResponse> ProjectToResponse(this IQueryable<ApiKey> apiKeys);
|
|
||||||
}
|
|
||||||
@@ -1,19 +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
|
|
||||||
{
|
|
||||||
// Mappers
|
|
||||||
public static partial ThemeResponse ToResponse(Theme theme);
|
|
||||||
public static partial Theme ToTheme(CreateThemeRequest request);
|
|
||||||
public static partial void Merge([MappingTarget] Theme theme, UpdateThemeRequest request);
|
|
||||||
|
|
||||||
// EF Relations
|
|
||||||
|
|
||||||
public static partial IQueryable<ThemeResponse> ProjectToResponse(this IQueryable<Theme> themes);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
|
||||||
using Riok.Mapperly.Abstractions;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Mappers;
|
|
||||||
|
|
||||||
[Mapper]
|
|
||||||
public static partial class UserMapper
|
|
||||||
{
|
|
||||||
// Mappers
|
|
||||||
public static partial UserResponse ToResponse(User user);
|
|
||||||
|
|
||||||
// EF Relations
|
|
||||||
public static partial IQueryable<UserResponse> ProjectToResponse(this IQueryable<User> users);
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Moonlight.ApiServer.Models;
|
|
||||||
|
|
||||||
public class FrontendConfigurationOption
|
|
||||||
{
|
|
||||||
public string[] Scripts { get; set; } = [];
|
|
||||||
public string[] Styles { get; set; } = [];
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +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\"/>
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<PackageId>Moonlight.ApiServer</PackageId>
|
|
||||||
<Version>2.1.15</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="Hangfire.AspNetCore" Version="1.8.20"/>
|
|
||||||
<PackageReference Include="Hangfire.Core" Version="1.8.20"/>
|
|
||||||
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
|
|
||||||
<PackageReference Include="MoonCore" Version="2.0.6" />
|
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.4.2" />
|
|
||||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
|
||||||
<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.3" />
|
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2"/>
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1"/>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Plugins;
|
|
||||||
|
|
||||||
public interface IPluginStartup
|
|
||||||
{
|
|
||||||
public void AddPlugin(WebApplicationBuilder builder);
|
|
||||||
public void UsePlugin(WebApplication app);
|
|
||||||
public void MapPlugin(WebApplication app);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Services;
|
|
||||||
|
|
||||||
public class ApiKeyAuthService
|
|
||||||
{
|
|
||||||
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
|
||||||
|
|
||||||
public ApiKeyAuthService(DatabaseRepository<ApiKey> apiKeyRepository)
|
|
||||||
{
|
|
||||||
ApiKeyRepository = apiKeyRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
|
|
||||||
{
|
|
||||||
// Ignore malformed claims principal
|
|
||||||
if (principal is not { Identity.IsAuthenticated: true })
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var apiKeyIdStr = principal.FindFirstValue("ApiKeyId");
|
|
||||||
|
|
||||||
if (!int.TryParse(apiKeyIdStr, out var apiKeyId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return await ApiKeyRepository
|
|
||||||
.Get()
|
|
||||||
.AnyAsync(x => x.Id == apiKeyId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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> GetOsNameAsync()
|
|
||||||
{
|
|
||||||
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> GetMemoryUsageAsync()
|
|
||||||
{
|
|
||||||
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> GetUptimeAsync()
|
|
||||||
{
|
|
||||||
var process = Process.GetCurrentProcess();
|
|
||||||
var uptime = DateTime.Now - process.StartTime;
|
|
||||||
return Task.FromResult(uptime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<int> GetCpuUsageAsync()
|
|
||||||
{
|
|
||||||
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 ShutdownAsync()
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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[]> GetProvidersAsync()
|
|
||||||
{
|
|
||||||
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> GenerateDiagnoseAsync(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.ModifyZipArchiveAsync(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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> GetConfigurationAsync()
|
|
||||||
{
|
|
||||||
var configuration = new FrontendConfiguration()
|
|
||||||
{
|
|
||||||
ApiUrl = Configuration.PublicUrl,
|
|
||||||
HostEnvironment = "ApiServer"
|
|
||||||
};
|
|
||||||
|
|
||||||
return Task.FromResult(configuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> GenerateIndexHtmlAsync() // 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.RenderToHtmlAsync<FrontendPage>(
|
|
||||||
ServiceProvider,
|
|
||||||
parameters =>
|
|
||||||
{
|
|
||||||
parameters["Theme"] = theme!;
|
|
||||||
parameters["Styles"] = styles;
|
|
||||||
parameters["Scripts"] = scripts;
|
|
||||||
parameters["Title"] = "Moonlight"; // TODO: Config
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Stream> GenerateZipAsync() // 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 ArchiveFsItemAsync(zipArchive, wasmPath, wasmPath);
|
|
||||||
|
|
||||||
// Add blazor files
|
|
||||||
await ArchiveFsItemAsync(zipArchive, blazorPath, blazorPath, "_framework/");
|
|
||||||
|
|
||||||
// Add frontend.json
|
|
||||||
var frontendConfig = await GetConfigurationAsync();
|
|
||||||
frontendConfig.HostEnvironment = "Static";
|
|
||||||
var frontendJson = JsonSerializer.Serialize(frontendConfig);
|
|
||||||
await ArchiveTextAsync(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 ArchiveFsItemAsync(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 ArchiveFsItemAsync(archive, directoryItem, prefixToRemove, prefixToAdd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ArchiveTextAsync(ZipArchive archive, string path, string content)
|
|
||||||
{
|
|
||||||
var data = Encoding.UTF8.GetBytes(content);
|
|
||||||
await ArchiveBytesAsync(archive, path, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ArchiveBytesAsync(ZipArchive archive, string path, byte[] bytes)
|
|
||||||
{
|
|
||||||
var entry = archive.CreateEntry(path);
|
|
||||||
await using var dataStream = entry.Open();
|
|
||||||
|
|
||||||
await dataStream.WriteAsync(bytes);
|
|
||||||
await dataStream.FlushAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 InitializeAsync()
|
|
||||||
{
|
|
||||||
Logger.LogDebug(
|
|
||||||
"Initializing metrics: {names}",
|
|
||||||
string.Join(", ", Metrics.Select(x => x.GetType().FullName))
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach (var metric in Metrics)
|
|
||||||
await metric.InitializeAsync(Meter);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
await InitializeAsync();
|
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
using var scope = ServiceProvider.CreateScope();
|
|
||||||
|
|
||||||
foreach (var metric in Metrics)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await metric.RunAsync(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.OpenTelemetry.Metrics.Interval),
|
|
||||||
stoppingToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using MoonCore.Extended.Helpers;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.ApiServer.Interfaces;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Services;
|
|
||||||
|
|
||||||
public class UserAuthService
|
|
||||||
{
|
|
||||||
private readonly ILogger<UserAuthService> Logger;
|
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
private readonly IEnumerable<IUserAuthExtension> Extensions;
|
|
||||||
|
|
||||||
private const string UserIdClaim = "UserId";
|
|
||||||
private const string IssuedAtClaim = "IssuedAt";
|
|
||||||
|
|
||||||
public UserAuthService(
|
|
||||||
ILogger<UserAuthService> logger,
|
|
||||||
DatabaseRepository<User> userRepository,
|
|
||||||
AppConfiguration configuration,
|
|
||||||
IEnumerable<IUserAuthExtension> extensions
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
UserRepository = userRepository;
|
|
||||||
Configuration = configuration;
|
|
||||||
Extensions = extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
|
||||||
{
|
|
||||||
// Ignore malformed claims principal
|
|
||||||
if (principal is not { Identity.IsAuthenticated: true })
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Search for email and username. We need both to create the user model if required.
|
|
||||||
// We do a ToLower here because external authentication provider might provide case-sensitive data
|
|
||||||
|
|
||||||
var email = principal.FindFirstValue(ClaimTypes.Email)?.ToLower();
|
|
||||||
var username = principal.FindFirstValue(ClaimTypes.Name)?.ToLower();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(username))
|
|
||||||
{
|
|
||||||
Logger.LogWarning(
|
|
||||||
"The authentication scheme {scheme} did not provide claim types: email, name. These are required to sync to user to the database",
|
|
||||||
principal.Identity.AuthenticationType
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If you plan to use multiple auth providers it can be a good idea
|
|
||||||
// to use an identifier in the user model which consists of the provider and the NameIdentifier
|
|
||||||
// instead of the email address. For simplicity, we just use the email as the identifier so multiple auth providers
|
|
||||||
// can lead to the same account when the email matches
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(u => u.Email == email);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
string[] permissions = [];
|
|
||||||
|
|
||||||
// Yes I know we handle the first user admin thing in the LocalAuth too,
|
|
||||||
// but this only works fo the local auth. So if a user uses an external auth scheme
|
|
||||||
// like oauth2 discord, the first user admin toggle would do nothing
|
|
||||||
if (Configuration.Authentication.FirstUserAdmin)
|
|
||||||
{
|
|
||||||
var count = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.CountAsync();
|
|
||||||
|
|
||||||
if (count == 0)
|
|
||||||
permissions = ["*"];
|
|
||||||
}
|
|
||||||
|
|
||||||
user = await UserRepository.AddAsync(new User()
|
|
||||||
{
|
|
||||||
Email = email,
|
|
||||||
TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1),
|
|
||||||
Username = username,
|
|
||||||
Password = HashHelper.Hash(Formatter.GenerateString(64)),
|
|
||||||
Permissions = permissions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can sync other properties here
|
|
||||||
if (user.Username != username)
|
|
||||||
{
|
|
||||||
user.Username = username;
|
|
||||||
await UserRepository.UpdateAsync(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich claims with required metadata
|
|
||||||
principal.Identities.First().AddClaims([
|
|
||||||
new Claim(UserIdClaim, user.Id.ToString()),
|
|
||||||
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()),
|
|
||||||
new Claim("Permissions", string.Join(';', user.Permissions))
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Call extensions
|
|
||||||
foreach (var extension in Extensions)
|
|
||||||
{
|
|
||||||
var result = await extension.SyncAsync(user, principal);
|
|
||||||
|
|
||||||
if (!result) // Exit immediately if result is false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
|
|
||||||
{
|
|
||||||
// Ignore malformed claims principal
|
|
||||||
if (principal is not { Identity.IsAuthenticated: true })
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Validate if the user still exists, and then we want to validate the token issue time
|
|
||||||
// against the invalidation time
|
|
||||||
|
|
||||||
var userIdStr = principal.FindFirstValue(UserIdClaim);
|
|
||||||
|
|
||||||
if (!int.TryParse(userIdStr, out var userId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == userId);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Token time validation
|
|
||||||
var issuedAtStr = principal.FindFirstValue(IssuedAtClaim);
|
|
||||||
|
|
||||||
if (!long.TryParse(issuedAtStr, out var issuedAtUnix))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var issuedAt = DateTimeOffset
|
|
||||||
.FromUnixTimeSeconds(issuedAtUnix)
|
|
||||||
.ToUniversalTime();
|
|
||||||
|
|
||||||
// If the issued at timestamp is greater than the token validation timestamp
|
|
||||||
// everything is fine. If not it means that the token should be invalidated
|
|
||||||
// as it is too old
|
|
||||||
|
|
||||||
if (issuedAt < user.TokenValidTimestamp)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Call extensions
|
|
||||||
foreach (var extension in Extensions)
|
|
||||||
{
|
|
||||||
var result = await extension.ValidateAsync(user, principal);
|
|
||||||
|
|
||||||
if (!result) // Exit immediately if result is false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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> ValidateAsync(User user)
|
|
||||||
{
|
|
||||||
foreach (var handler in Handlers)
|
|
||||||
{
|
|
||||||
var result = await handler.ValidateAsync(user);
|
|
||||||
|
|
||||||
if (!result.IsAllowed)
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserDeleteValidationResult.Allow();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAsync(User user, bool force)
|
|
||||||
{
|
|
||||||
foreach (var handler in Handlers)
|
|
||||||
await handler.DeleteAsync(user, force);
|
|
||||||
|
|
||||||
await UserRepository.RemoveAsync(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using MoonCore.Permissions;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Implementations.LocalAuth;
|
|
||||||
using Moonlight.ApiServer.Services;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void AddAuth(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
builder.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
builder.Services
|
|
||||||
.AddAuthentication(options => { options.DefaultScheme = "MainScheme"; })
|
|
||||||
.AddPolicyScheme("MainScheme", null, options =>
|
|
||||||
{
|
|
||||||
// If an api key is specified via the bearer auth header
|
|
||||||
// we want to use the ApiKey scheme for authenticating the request
|
|
||||||
options.ForwardDefaultSelector = context =>
|
|
||||||
{
|
|
||||||
var headers = context.Request.Headers;
|
|
||||||
|
|
||||||
// For regular api calls
|
|
||||||
if (headers.ContainsKey("Authorization"))
|
|
||||||
return "ApiKey";
|
|
||||||
|
|
||||||
// For websocket requests which cannot use the Authorization header
|
|
||||||
if (headers.Upgrade == "websocket" && headers.Connection == "Upgrade" && context.Request.Query.ContainsKey("access_token"))
|
|
||||||
return "ApiKey";
|
|
||||||
|
|
||||||
// Regular user traffic/auth
|
|
||||||
return "Session";
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.AddJwtBearer("ApiKey", null, 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
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Events = new JwtBearerEvents()
|
|
||||||
{
|
|
||||||
OnTokenValidated = async context =>
|
|
||||||
{
|
|
||||||
var apiKeyAuthService = context
|
|
||||||
.HttpContext
|
|
||||||
.RequestServices
|
|
||||||
.GetRequiredService<ApiKeyAuthService>();
|
|
||||||
|
|
||||||
var result = await apiKeyAuthService.ValidateAsync(context.Principal);
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
context.Fail("API key has been deleted");
|
|
||||||
},
|
|
||||||
|
|
||||||
OnMessageReceived = context =>
|
|
||||||
{
|
|
||||||
var accessToken = context.Request.Query["access_token"];
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(accessToken))
|
|
||||||
context.Token = accessToken;
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.AddCookie("Session", null, options =>
|
|
||||||
{
|
|
||||||
options.ExpireTimeSpan = TimeSpan.FromDays(configuration.Authentication.Sessions.ExpiresIn);
|
|
||||||
|
|
||||||
options.Cookie = new CookieBuilder()
|
|
||||||
{
|
|
||||||
Name = configuration.Authentication.Sessions.CookieName,
|
|
||||||
Path = "/",
|
|
||||||
IsEssential = true,
|
|
||||||
SecurePolicy = CookieSecurePolicy.SameAsRequest
|
|
||||||
};
|
|
||||||
|
|
||||||
// As redirects won't work in our spa which uses API calls
|
|
||||||
// we need to customize the responses when certain actions happen
|
|
||||||
options.Events.OnRedirectToLogin = async context =>
|
|
||||||
{
|
|
||||||
await Results.Problem(
|
|
||||||
title: "Unauthenticated",
|
|
||||||
detail: "You need to authenticate yourself to use this endpoint",
|
|
||||||
statusCode: 401
|
|
||||||
)
|
|
||||||
.ExecuteAsync(context.HttpContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Events.OnRedirectToAccessDenied = async context =>
|
|
||||||
{
|
|
||||||
await Results.Problem(
|
|
||||||
title: "Permission denied",
|
|
||||||
detail: "You are missing the required permissions to access this endpoint",
|
|
||||||
statusCode: 403
|
|
||||||
)
|
|
||||||
.ExecuteAsync(context.HttpContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Events.OnSigningIn = async context =>
|
|
||||||
{
|
|
||||||
var userSyncService = context
|
|
||||||
.HttpContext
|
|
||||||
.RequestServices
|
|
||||||
.GetRequiredService<UserAuthService>();
|
|
||||||
|
|
||||||
var result = await userSyncService.SyncAsync(context.Principal);
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
context.Principal = new();
|
|
||||||
else
|
|
||||||
context.Properties.IsPersistent = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Events.OnValidatePrincipal = async context =>
|
|
||||||
{
|
|
||||||
var userSyncService = context
|
|
||||||
.HttpContext
|
|
||||||
.RequestServices
|
|
||||||
.GetRequiredService<UserAuthService>();
|
|
||||||
|
|
||||||
var result = await userSyncService.ValidateAsync(context.Principal);
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
context.RejectPrincipal();
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.AddScheme<LocalAuthOptions, LocalAuthHandler>(LocalAuthConstants.AuthenticationScheme, "Local Auth", options =>
|
|
||||||
{
|
|
||||||
options.ForwardAuthenticate = "Session";
|
|
||||||
options.ForwardSignIn = "Session";
|
|
||||||
options.ForwardSignOut = "Session";
|
|
||||||
|
|
||||||
options.SignInScheme = "Session";
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddAuthorization();
|
|
||||||
|
|
||||||
builder.Services.AddAuthorizationPermissions(options =>
|
|
||||||
{
|
|
||||||
options.ClaimName = "Permissions";
|
|
||||||
options.Prefix = "permissions:";
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddScoped<UserAuthService>();
|
|
||||||
builder.Services.AddScoped<ApiKeyAuthService>();
|
|
||||||
|
|
||||||
// Setup data protection storage within storage folder
|
|
||||||
// so its persists in containers
|
|
||||||
var dpKeyPath = Path.Combine("storage", "dataProtectionKeys");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(dpKeyPath);
|
|
||||||
|
|
||||||
builder.Services
|
|
||||||
.AddDataProtection()
|
|
||||||
.PersistKeysToFileSystem(
|
|
||||||
new DirectoryInfo(dpKeyPath)
|
|
||||||
);
|
|
||||||
|
|
||||||
builder.Services.AddScoped<UserDeletionService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UseAuth(this WebApplication application)
|
|
||||||
{
|
|
||||||
application.UseAuthentication();
|
|
||||||
application.UseAuthorization();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using MoonCore.Extended.Extensions;
|
|
||||||
using MoonCore.Extensions;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Plugins;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void AddBase(this WebApplicationBuilder builder, IPluginStartup[] startups)
|
|
||||||
{
|
|
||||||
builder.Services.AutoAddServices<IAssemblyMarker>();
|
|
||||||
builder.Services.AddHttpClient();
|
|
||||||
|
|
||||||
builder.Services.AddApiExceptionHandler();
|
|
||||||
|
|
||||||
// Configure controllers
|
|
||||||
var mvcBuilder = builder.Services.AddControllers();
|
|
||||||
|
|
||||||
// Add plugin assemblies as application parts
|
|
||||||
foreach (var pluginStartup in startups.Select(x => x.GetType().Assembly).Distinct())
|
|
||||||
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UseBase(this WebApplication application)
|
|
||||||
{
|
|
||||||
application.UseRouting();
|
|
||||||
application.UseExceptionHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MapBase(this WebApplication application)
|
|
||||||
{
|
|
||||||
application.MapControllers();
|
|
||||||
|
|
||||||
// Frontend
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
application.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
if (configuration.Frontend.EnableHosting)
|
|
||||||
application.MapFallbackToController("Index", "Frontend");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureKestrel(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
builder.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
builder.WebHost.ConfigureKestrel(kestrelOptions =>
|
|
||||||
{
|
|
||||||
var maxUploadInBytes = ByteConverter
|
|
||||||
.FromMegaBytes(configuration.Kestrel.UploadLimit)
|
|
||||||
.Bytes;
|
|
||||||
|
|
||||||
kestrelOptions.Limits.MaxRequestBodySize = maxUploadInBytes;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using MoonCore.EnvConfiguration;
|
|
||||||
using MoonCore.Yaml;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void AddConfiguration(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
// Yaml
|
|
||||||
var yamlPath = Path.Combine("storage", "config.yml");
|
|
||||||
|
|
||||||
YamlDefaultGenerator.GenerateAsync<AppConfiguration>(yamlPath).Wait();
|
|
||||||
|
|
||||||
builder.Configuration.AddYamlFile(yamlPath);
|
|
||||||
|
|
||||||
// Env
|
|
||||||
builder.Configuration.AddEnvironmentVariables(prefix: "MOONLIGHT_", separator: "_");
|
|
||||||
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
builder.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
builder.Services.AddSingleton(configuration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using MoonCore.Extended.Extensions;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void AddDatabase(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
builder.Services.AddDbAutoMigrations();
|
|
||||||
builder.Services.AddDatabaseMappings();
|
|
||||||
|
|
||||||
builder.Services.AddScoped(typeof(DatabaseRepository<>));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using Hangfire;
|
|
||||||
using Hangfire.EntityFrameworkCore;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Moonlight.ApiServer.Database;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void AddMoonlightHangfire(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
builder.Services.AddHangfire((provider, configuration) =>
|
|
||||||
{
|
|
||||||
configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
|
|
||||||
configuration.UseSimpleAssemblyNameTypeSerializer();
|
|
||||||
configuration.UseRecommendedSerializerSettings();
|
|
||||||
configuration.UseEFCoreStorage(() =>
|
|
||||||
{
|
|
||||||
var scope = provider.CreateScope();
|
|
||||||
return scope.ServiceProvider.GetRequiredService<CoreDataContext>();
|
|
||||||
}, new EFCoreStorageOptions());
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddHangfireServer();
|
|
||||||
|
|
||||||
builder.Logging.AddFilter(
|
|
||||||
"Hangfire.Server.BackgroundServerProcess",
|
|
||||||
LogLevel.Warning
|
|
||||||
);
|
|
||||||
|
|
||||||
builder.Logging.AddFilter(
|
|
||||||
"Hangfire.BackgroundJobServer",
|
|
||||||
LogLevel.Warning
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UseMoonlightHangfire(this WebApplication application)
|
|
||||||
{
|
|
||||||
if (application.Environment.IsDevelopment())
|
|
||||||
application.UseHangfireDashboard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MoonCore.Logging;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void AddLogging(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
// Logging providers
|
|
||||||
builder.Logging.ClearProviders();
|
|
||||||
|
|
||||||
builder.Logging.AddAnsiConsole();
|
|
||||||
builder.Logging.AddFile(Path.Combine("storage", "logs", "moonlight.log"));
|
|
||||||
|
|
||||||
// Logging levels
|
|
||||||
var logConfigPath = Path.Combine("storage", "logConfig.json");
|
|
||||||
|
|
||||||
// Ensure default log levels exist
|
|
||||||
if (!File.Exists(logConfigPath))
|
|
||||||
{
|
|
||||||
var defaultLogLevels = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "Default", "Information" },
|
|
||||||
{ "Microsoft.AspNetCore", "Warning" },
|
|
||||||
{ "System.Net.Http.HttpClient", "Warning" },
|
|
||||||
{ "Moonlight.ApiServer.Implementations.LocalAuth.LocalAuthHandler", "Warning" }
|
|
||||||
};
|
|
||||||
|
|
||||||
var logLevelsJson = JsonSerializer.Serialize(defaultLogLevels);
|
|
||||||
File.WriteAllText(logConfigPath, logLevelsJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read log levels
|
|
||||||
var logLevels = JsonSerializer.Deserialize<Dictionary<string, string>>(
|
|
||||||
File.ReadAllText(logConfigPath)
|
|
||||||
)!;
|
|
||||||
|
|
||||||
// Apply configured log levels
|
|
||||||
foreach (var level in logLevels)
|
|
||||||
builder.Logging.AddFilter(level.Key, Enum.Parse<LogLevel>(level.Value));
|
|
||||||
|
|
||||||
// Mute exception handler middleware
|
|
||||||
// https://github.com/dotnet/aspnetcore/issues/19740
|
|
||||||
builder.Logging.AddFilter(
|
|
||||||
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
|
|
||||||
LogLevel.Critical
|
|
||||||
);
|
|
||||||
|
|
||||||
builder.Logging.AddFilter(
|
|
||||||
"Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware",
|
|
||||||
LogLevel.Critical
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void PrintVersionAsync()
|
|
||||||
{
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CreateStorageAsync()
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory("storage");
|
|
||||||
Directory.CreateDirectory(Path.Combine("storage", "logs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddMoonlightCors(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
builder.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
var allowedOrigins = configuration.Kestrel.AllowedOrigins;
|
|
||||||
|
|
||||||
builder.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()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UseMoonlightCors(this WebApplication application)
|
|
||||||
{
|
|
||||||
application.UseCors();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Moonlight.ApiServer.Plugins;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void AddPlugins(this WebApplicationBuilder builder, IPluginStartup[] startups)
|
|
||||||
{
|
|
||||||
foreach (var startup in startups)
|
|
||||||
startup.AddPlugin(builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UsePlugins(this WebApplication application, IPluginStartup[] startups)
|
|
||||||
{
|
|
||||||
foreach (var startup in startups)
|
|
||||||
startup.UsePlugin(application);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MapPlugins(this WebApplication application, IPluginStartup[] startups)
|
|
||||||
{
|
|
||||||
foreach (var startup in startups)
|
|
||||||
startup.MapPlugin(application);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Http.Hubs;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
private static void AddMoonlightSignalR(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
var configuration = AppConfiguration.CreateEmpty();
|
|
||||||
builder.Configuration.Bind(configuration);
|
|
||||||
|
|
||||||
var signalRBuilder = builder.Services.AddSignalR();
|
|
||||||
|
|
||||||
if (configuration.SignalR.UseRedis)
|
|
||||||
signalRBuilder.AddStackExchangeRedis(configuration.SignalR.RedisConnectionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MapMoonlightSignalR(this WebApplication application)
|
|
||||||
{
|
|
||||||
application.MapHub<DiagnoseHub>("/api/admin/system/diagnose/ws");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Moonlight.ApiServer.Plugins;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
|
||||||
|
|
||||||
public static partial class Startup
|
|
||||||
{
|
|
||||||
public static void AddMoonlight(this WebApplicationBuilder builder, IPluginStartup[] startups)
|
|
||||||
{
|
|
||||||
PrintVersionAsync();
|
|
||||||
CreateStorageAsync();
|
|
||||||
|
|
||||||
builder.AddConfiguration();
|
|
||||||
builder.AddLogging();
|
|
||||||
|
|
||||||
builder.ConfigureKestrel();
|
|
||||||
builder.AddBase(startups);
|
|
||||||
builder.AddDatabase();
|
|
||||||
builder.AddAuth();
|
|
||||||
builder.AddMoonlightCors();
|
|
||||||
builder.AddMoonlightHangfire();
|
|
||||||
builder.AddMoonlightSignalR();
|
|
||||||
|
|
||||||
builder.AddPlugins(startups);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UseMoonlight(this WebApplication application, IPluginStartup[] startups)
|
|
||||||
{
|
|
||||||
application.UseBase();
|
|
||||||
application.UseMoonlightCors();
|
|
||||||
application.UseAuth();
|
|
||||||
application.UseMoonlightHangfire();
|
|
||||||
application.UsePlugins(startups);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void MapMoonlight(this WebApplication application, IPluginStartup[] startups)
|
|
||||||
{
|
|
||||||
application.MapBase();
|
|
||||||
application.MapMoonlightSignalR();
|
|
||||||
application.MapPlugins(startups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user