Recreated solution with web app template. Improved theme. Switched to ShadcnBlazor library
This commit is contained in:
30
.dockerignore
Normal file
30
.dockerignore
Normal file
@@ -0,0 +1,30 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
**/appsettings.json
|
||||
**/appsettings.Development.json
|
||||
**/appsettings*
|
||||
**/compose.*
|
||||
**/.env.example
|
||||
LICENSE
|
||||
README.md
|
||||
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_USERNAME=user
|
||||
DATABASE_PASSWORD=user
|
||||
DATABASE_DATABASE=user
|
||||
OIDC_AUTHORITY=http://localhost:8092
|
||||
OIDC_AUTHORITY=http://localhost:8092
|
||||
OIDC_CLIENT_ID=client_id
|
||||
OIDC_CLIENT_SECRET=client_secret
|
||||
OIDC_REQUIRE_HTTPS_METADATA=false
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
ko_fi: masuowo
|
||||
87
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
87
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,87 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Something isn't working quite right in the software.
|
||||
labels: [not confirmed]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Bug reports should only be used for reporting issues with how the software works. For assistance installing this software, as well as debugging issues with dependencies, please use our [Discord server](https://discord.gg/TJaspT7A8p).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: Please provide a clear & concise description of the issue.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Please describe what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Please be as detailed as possible when providing steps to reproduce, failure to provide steps will result in this issue being closed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: panel-version
|
||||
attributes:
|
||||
label: Panel Version
|
||||
description: Version number of your Panel (latest is not a version)
|
||||
placeholder: v2 EA
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: daemon-version
|
||||
attributes:
|
||||
label: Daemon Version
|
||||
description: Version number of your Daemon (latest is not a version)
|
||||
placeholder: v2 EA
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: image-details
|
||||
attributes:
|
||||
label: Games and/or Images Affected
|
||||
description: Please include the specific game(s) or Image(s) you are running into this bug with.
|
||||
placeholder: Minecraft (Paper), Minecraft (Forge)
|
||||
|
||||
- type: input
|
||||
id: docker-image
|
||||
attributes:
|
||||
label: Docker Image
|
||||
description: The specific Docker image you are using for the game(s) above.
|
||||
placeholder: ghcr.io/xxx/yolks:java_17
|
||||
|
||||
- type: textarea
|
||||
id: panel-logs
|
||||
attributes:
|
||||
label: Error Logs
|
||||
description: |
|
||||
Run the following command to collect logs on your system.
|
||||
|
||||
Panel: `docker logs moonlight`
|
||||
Wings: `sudo wings diagnostics`
|
||||
placeholder: logs here
|
||||
render: bash
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please [search here](https://github.com/Moonlight-Panel/Moonlight/issues) to see if an issue already exists for your problem.
|
||||
options:
|
||||
- label: I have searched the existing issues before opening this issue.
|
||||
required: true
|
||||
- label: I have provided all relevant details, including the specific game and Docker images I am using if this issue is related to running a server.
|
||||
required: true
|
||||
- label: I have checked in the Discord server and believe this is a bug with the software, and not a configuration issue with my specific system.
|
||||
required: true
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Installation Help
|
||||
url: https://discord.gg/TJaspT7A8p
|
||||
about: Please visit our Discord for help with your installation.
|
||||
- name: General Question
|
||||
url: https://discord.gg/TJaspT7A8p
|
||||
about: Please visit our Discord for general questions about Moonlight Panel.
|
||||
32
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
32
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement for the software.
|
||||
labels: [feature request]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing feature request for this?
|
||||
description: Please [search here](https://github.com/Moonlight-Panel/Moonlight/issues?q=is%3Aissue) to see if someone else has already suggested this.
|
||||
options:
|
||||
- label: I have searched the existing issues before opening this feature request.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you would like to see.
|
||||
description: "A clear & concise description of the feature you'd like to have added, and what issues it would solve."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like.
|
||||
description: "You must explain how you'd like to see this feature implemented. Technical implementation details are not necessary, rather an idea of how you'd like to see this feature used."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context to this request.
|
||||
description: "Add any other context or screenshots about the feature request."
|
||||
validations:
|
||||
required: false
|
||||
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
|
||||
43
.gitignore
vendored
43
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
@@ -395,40 +395,13 @@ FodyWeavers.xsd
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Moonlight
|
||||
storage/
|
||||
**/.idea/**
|
||||
style.min.css
|
||||
|
||||
# Build script for nuget packages
|
||||
finalPackages/
|
||||
nupkgs/
|
||||
# Style builds
|
||||
**/style.min.css
|
||||
**/package-lock.json
|
||||
|
||||
# Scripts
|
||||
**/bin/**
|
||||
**/obj/**
|
||||
# Secrets
|
||||
**/.env
|
||||
**/appsettings.json
|
||||
**/appsettings.Development.json
|
||||
10
Hosts/Moonlight.Api.Host/AppStartupLoader.cs
Normal file
10
Hosts/Moonlight.Api.Host/AppStartupLoader.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using MoonCore.PluginFramework;
|
||||
using Moonlight.Api.Startup;
|
||||
|
||||
namespace Moonlight.Api.Host;
|
||||
|
||||
[PluginLoader]
|
||||
public partial class AppStartupLoader : IAppStartup
|
||||
{
|
||||
|
||||
}
|
||||
63
Hosts/Moonlight.Api.Host/Dockerfile
Normal file
63
Hosts/Moonlight.Api.Host/Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
||||
# Base image
|
||||
FROM cgr.dev/chainguard/aspnet-runtime:latest AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
|
||||
# Build dependencies
|
||||
RUN apt-get update; apt-get install nodejs npm -y; apt-get clean
|
||||
|
||||
# Build options
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
|
||||
# Download npm packages
|
||||
WORKDIR /src/Moonlight.Frontend/Styles
|
||||
|
||||
COPY ["Moonlight.Frontend/Styles/package.json", "package.json"]
|
||||
|
||||
RUN npm install
|
||||
|
||||
# Restore nuget packages
|
||||
WORKDIR /src
|
||||
|
||||
COPY ["Moonlight.Api/Moonlight.Api.csproj", "Moonlight.Api/"]
|
||||
COPY ["Moonlight.Frontend/Moonlight.Frontend.csproj", "Moonlight.Frontend/"]
|
||||
COPY ["Moonlight.Shared/Moonlight.Shared.csproj", "Moonlight.Shared/"]
|
||||
|
||||
RUN dotnet restore "Moonlight.Api/Moonlight.Api.csproj"
|
||||
RUN dotnet restore "Moonlight.Frontend/Moonlight.Frontend.csproj"
|
||||
|
||||
# Copy over the whole sources
|
||||
COPY . .
|
||||
|
||||
# Build styles
|
||||
WORKDIR /src/Moonlight.Frontend/Styles
|
||||
RUN npm run tailwind-build
|
||||
|
||||
# Build projects
|
||||
WORKDIR "/src/Moonlight.Api"
|
||||
RUN dotnet build "./Moonlight.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build-api
|
||||
|
||||
WORKDIR "/src/Moonlight.Frontend"
|
||||
RUN dotnet build "./Moonlight.Frontend.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
|
||||
|
||||
FROM build AS publish
|
||||
|
||||
# Publish options
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
|
||||
# Publish applications
|
||||
WORKDIR "/src/Moonlight.Api"
|
||||
RUN dotnet publish "./Moonlight.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish-api /p:UseAppHost=false
|
||||
|
||||
WORKDIR "/src/Moonlight.Frontend"
|
||||
RUN dotnet publish "./Moonlight.Frontend.csproj" -c $BUILD_CONFIGURATION -o /app/publish-frontend /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=publish /app/publish-api .
|
||||
COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot
|
||||
|
||||
ENTRYPOINT ["dotnet", "Moonlight.Api.dll"]
|
||||
32
Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj
Normal file
32
Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj
Normal file
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
|
||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Moonlight.Api\Moonlight.Api.csproj"/>
|
||||
<ProjectReference Include="..\Moonlight.Frontend.Host\Moonlight.Frontend.Host.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
22
Hosts/Moonlight.Api.Host/Program.cs
Normal file
22
Hosts/Moonlight.Api.Host/Program.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Moonlight.Api.Host;
|
||||
|
||||
var appLoader = new AppStartupLoader();
|
||||
appLoader.Initialize();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
appLoader.PreBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
appLoader.PostBuild(app);
|
||||
|
||||
appLoader.PostMiddleware(app);
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
app.UseWebAssemblyDebugging();
|
||||
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
await app.RunAsync();
|
||||
14
Hosts/Moonlight.Api.Host/Properties/launchSettings.json
Normal file
14
Hosts/Moonlight.Api.Host/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5240",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Hosts/Moonlight.Frontend.Host/AppStartupLoader.cs
Normal file
10
Hosts/Moonlight.Frontend.Host/AppStartupLoader.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using MoonCore.PluginFramework;
|
||||
using Moonlight.Frontend.Startup;
|
||||
|
||||
namespace Moonlight.Frontend.Host;
|
||||
|
||||
[PluginLoader]
|
||||
public partial class AppStartupLoader : IAppStartup
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
|
||||
|
||||
<StaticWebAssetsEnabled>True</StaticWebAssetsEnabled>
|
||||
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||
<WasmEnableSIMD>true</WasmEnableSIMD>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
|
||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9" />
|
||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.9" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\css\" />
|
||||
<ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="Plugins.props" />
|
||||
|
||||
</Project>
|
||||
15
Hosts/Moonlight.Frontend.Host/Program.cs
Normal file
15
Hosts/Moonlight.Frontend.Host/Program.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Moonlight.Frontend.Host;
|
||||
|
||||
var appLoader = new AppStartupLoader();
|
||||
appLoader.Initialize();
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
appLoader.PreBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
appLoader.PostBuild(app);
|
||||
|
||||
await app.RunAsync();
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5165",
|
||||
"applicationUrl": "http://localhost:5250",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Hosts/Moonlight.Frontend.Host/Styles/package.json
Normal file
15
Hosts/Moonlight.Frontend.Host/Styles/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"scripts": {
|
||||
"tailwind": "npx postcss styles.css -o ../wwwroot/style.min.css --watch",
|
||||
"tailwind-build": "npx postcss styles.css -o ../wwwroot/style.min.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"cssnano": "^7.1.2",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"shadcnblazor": "^1.0.4",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
}
|
||||
}
|
||||
8
Hosts/Moonlight.Frontend.Host/Styles/postcss.config.mjs
Normal file
8
Hosts/Moonlight.Frontend.Host/Styles/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
"cssnano":{
|
||||
preset: 'default'
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Hosts/Moonlight.Frontend.Host/Styles/styles.css
Normal file
30
Hosts/Moonlight.Frontend.Host/Styles/styles.css
Normal file
@@ -0,0 +1,30 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@import "./node_modules/shadcnblazor/scrollbar.css";
|
||||
@import "./node_modules/shadcnblazor/default-theme.css";
|
||||
@import "./theme.css";
|
||||
@source "./node_modules/shadcnblazor/classes.json";
|
||||
|
||||
@source "../../Moonlight.Api/**/*.razor";
|
||||
@source "../../Moonlight.Api/**/*.cs";
|
||||
@source "../../Moonlight.Api/**/*.html";
|
||||
|
||||
@source "../../Moonlight.Frontend/**/*.razor";
|
||||
@source "../../Moonlight.Frontend/**/*.cs";
|
||||
@source "../../Moonlight.Frontend/**/*.html";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
#blazor-error-ui {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
132
Hosts/Moonlight.Frontend.Host/Styles/theme.css
Normal file
132
Hosts/Moonlight.Frontend.Host/Styles/theme.css
Normal file
@@ -0,0 +1,132 @@
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.129 0.042 264.695);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||
--primary: oklch(0.208 0.042 265.755);
|
||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
--muted: oklch(0.968 0.007 247.896);
|
||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||
--accent: oklch(0.968 0.007 247.896);
|
||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
--input: oklch(0.929 0.013 255.508);
|
||||
--ring: oklch(0.704 0.04 256.788);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
|
||||
--sidebar: var(--background);
|
||||
--sidebar-foreground: var(--foreground);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: var(--primary-foreground);
|
||||
--sidebar-accent: var(--accent);
|
||||
--sidebar-accent-foreground: var(--accent-foreground);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Deep blue-slate background with purple undertones */
|
||||
--background: oklch(0.16 0.028 260);
|
||||
--foreground: oklch(0.98 0.008 260);
|
||||
|
||||
/* Cards with slightly lighter blue-slate */
|
||||
--card: oklch(0.21 0.032 260);
|
||||
--card-foreground: oklch(0.98 0.008 260);
|
||||
|
||||
/* Popovers with medium depth */
|
||||
--popover: oklch(0.24 0.035 260);
|
||||
--popover-foreground: oklch(0.98 0.008 260);
|
||||
|
||||
/* Vibrant blue-purple primary */
|
||||
--primary: oklch(0.62 0.18 270);
|
||||
--primary-foreground: oklch(0.99 0.005 260);
|
||||
|
||||
/* Secondary with blue-slate tone */
|
||||
--secondary: oklch(0.27 0.038 260);
|
||||
--secondary-foreground: oklch(0.98 0.008 260);
|
||||
|
||||
/* Muted elements */
|
||||
--muted: oklch(0.25 0.035 260);
|
||||
--muted-foreground: oklch(0.66 0.025 260);
|
||||
|
||||
/* Accent with purple-blue blend */
|
||||
--accent: oklch(0.36 0.065 268);
|
||||
--accent-foreground: oklch(0.98 0.008 260);
|
||||
|
||||
/* Destructive red with good contrast */
|
||||
--destructive: oklch(0.62 0.22 25);
|
||||
--destructive-foreground: oklch(0.99 0.005 260);
|
||||
|
||||
/* Subtle borders and inputs */
|
||||
--border: oklch(0.32 0.025 260);
|
||||
--input: oklch(0.30 0.030 260);
|
||||
--ring: oklch(0.62 0.18 270);
|
||||
|
||||
/* Chart colors with blue-purple harmony */
|
||||
--chart-1: oklch(0.58 0.18 270);
|
||||
--chart-2: oklch(0.62 0.16 245);
|
||||
--chart-3: oklch(0.68 0.15 290);
|
||||
--chart-4: oklch(0.60 0.20 260);
|
||||
--chart-5: oklch(0.65 0.14 280);
|
||||
|
||||
/* Sidebar with slightly different depth */
|
||||
--sidebar: oklch(0.18 0.030 260);
|
||||
--sidebar-foreground: oklch(0.97 0.008 260);
|
||||
--sidebar-primary: oklch(0.60 0.17 270);
|
||||
--sidebar-primary-foreground: oklch(0.99 0.005 260);
|
||||
--sidebar-accent: oklch(0.26 0.038 260);
|
||||
--sidebar-accent-foreground: oklch(0.98 0.008 260);
|
||||
--sidebar-border: oklch(0.30 0.025 260);
|
||||
--sidebar-ring: oklch(0.58 0.17 270);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
58
Hosts/Moonlight.Frontend.Host/wwwroot/index.html
Normal file
58
Hosts/Moonlight.Frontend.Host/wwwroot/index.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Moonlight</title>
|
||||
<base href="/" />
|
||||
<link rel="preload" id="webassembly" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback" />
|
||||
<link rel="stylesheet" href="style.min.css" />
|
||||
<script type="importmap"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-background text-foreground">
|
||||
<div id="app">
|
||||
<div class="h-screen w-full flex items-center justify-center">
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-3 rounded-lg border-dashed p-6 text-center text-balance md:p-12">
|
||||
<div class="flex max-w-sm flex-col items-center gap-2 text-center">
|
||||
<div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground size-10 rounded-lg [&_svg:not([class*='size-'])]:size-6">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-zap-icon lucide-zap size-6">
|
||||
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-lg font-medium tracking-tight">
|
||||
Loading application
|
||||
</div>
|
||||
<div class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
|
||||
|
||||
<div class="bg-primary/20 w-full relative h-2 overflow-hidden rounded-full">
|
||||
<div class="bg-primary h-full w-[var(--blazor-load-percentage,0)] flex-1 transition-all">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
|
||||
<script src="/_content/ShadcnBlazor/interop.js" defer></script>
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
121
LICENSE
121
LICENSE
@@ -1,121 +0,0 @@
|
||||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
10
Moonlight.Api/Configuration/DatabaseOptions.cs
Normal file
10
Moonlight.Api/Configuration/DatabaseOptions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class DatabaseOptions
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; } = 5432;
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Database { get; set; }
|
||||
}
|
||||
11
Moonlight.Api/Configuration/OidcOptions.cs
Normal file
11
Moonlight.Api/Configuration/OidcOptions.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class OidcOptions
|
||||
{
|
||||
public string Authority { get; set; }
|
||||
public bool RequireHttpsMetadata { get; set; } = true;
|
||||
public string ResponseType { get; set; } = "code";
|
||||
public string[]? Scopes { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
}
|
||||
32
Moonlight.Api/Database/DataContext.cs
Normal file
32
Moonlight.Api/Database/DataContext.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
|
||||
namespace Moonlight.Api.Database;
|
||||
|
||||
public class DataContext : DbContext
|
||||
{
|
||||
public DbSet<User> Users { get; set; }
|
||||
|
||||
private readonly IOptions<DatabaseOptions> Options;
|
||||
|
||||
public DataContext(IOptions<DatabaseOptions> options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (optionsBuilder.IsConfigured)
|
||||
return;
|
||||
|
||||
optionsBuilder.UseNpgsql(
|
||||
$"Host={Options.Value.Host};" +
|
||||
$"Port={Options.Value.Port};" +
|
||||
$"Username={Options.Value.Username};" +
|
||||
$"Password={Options.Value.Password};" +
|
||||
$"Database={Options.Value.Database}"
|
||||
);
|
||||
}
|
||||
}
|
||||
36
Moonlight.Api/Database/DatabaseRepository.cs
Normal file
36
Moonlight.Api/Database/DatabaseRepository.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Moonlight.Api.Database;
|
||||
|
||||
public class DatabaseRepository<T> where T : class
|
||||
{
|
||||
private readonly DataContext DataContext;
|
||||
private readonly DbSet<T> Set;
|
||||
|
||||
public DatabaseRepository(DataContext dataContext)
|
||||
{
|
||||
DataContext = dataContext;
|
||||
Set = DataContext.Set<T>();
|
||||
}
|
||||
|
||||
public IQueryable<T> Query() => Set;
|
||||
|
||||
public async Task<T> AddAsync(T entity)
|
||||
{
|
||||
var final = Set.Add(entity);
|
||||
await DataContext.SaveChangesAsync();
|
||||
return final.Entity;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(T entity)
|
||||
{
|
||||
Set.Update(entity);
|
||||
await DataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(T entity)
|
||||
{
|
||||
Set.Remove(entity);
|
||||
await DataContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
11
Moonlight.Api/Database/Entities/User.cs
Normal file
11
Moonlight.Api/Database/Entities/User.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Moonlight.Api.Database.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Username { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
public DateTimeOffset InvalidateTimestamp { get; set; }
|
||||
}
|
||||
55
Moonlight.Api/Database/Migrations/20251216083232_AddedUsers.Designer.cs
generated
Normal file
55
Moonlight.Api/Database/Migrations/20251216083232_AddedUsers.Designer.cs
generated
Normal file
@@ -0,0 +1,55 @@
|
||||
// <auto-generated />
|
||||
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Moonlight.Api.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.Api.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20251216083232_AddedUsers")]
|
||||
partial class AddedUsers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("InvalidateTimestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.Api.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedUsers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Username = table.Column<string>(type: "text", nullable: false),
|
||||
Email = table.Column<string>(type: "text", nullable: false),
|
||||
InvalidateTimestamp =
|
||||
table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table => { table.PrimaryKey("PK_Users", x => x.Id); });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// <auto-generated />
|
||||
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Moonlight.Api.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.Api.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
partial class DataContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("InvalidateTimestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Moonlight.Api/Helpers/AppConsoleFormatter.cs
Normal file
77
Moonlight.Api/Helpers/AppConsoleFormatter.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
|
||||
namespace Moonlight.Api.Helpers;
|
||||
|
||||
public class AppConsoleFormatter : ConsoleFormatter
|
||||
{
|
||||
private const string TimestampColor = "\e[38;2;148;148;148m";
|
||||
private const string CategoryColor = "\e[38;2;198;198;198m";
|
||||
private const string MessageColor = "\e[38;2;255;255;255m";
|
||||
private const string Bold = "\e[1m";
|
||||
|
||||
// Pre-computed ANSI color codes for each log level
|
||||
private const string CriticalColor = "\e[38;2;255;0;0m";
|
||||
private const string ErrorColor = "\e[38;2;255;0;0m";
|
||||
private const string WarningColor = "\e[38;2;215;215;0m";
|
||||
private const string InfoColor = "\e[38;2;135;215;255m";
|
||||
private const string DebugColor = "\e[38;2;198;198;198m";
|
||||
private const string TraceColor = "\e[38;2;68;68;68m";
|
||||
|
||||
public AppConsoleFormatter() : base(nameof(AppConsoleFormatter))
|
||||
{
|
||||
}
|
||||
|
||||
public override void Write<TState>(
|
||||
in LogEntry<TState> logEntry,
|
||||
IExternalScopeProvider? scopeProvider,
|
||||
TextWriter textWriter)
|
||||
{
|
||||
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
|
||||
|
||||
// Timestamp
|
||||
textWriter.Write(TimestampColor);
|
||||
textWriter.Write(DateTime.Now.ToString("dd.MM.yy HH:mm:ss"));
|
||||
textWriter.Write(' ');
|
||||
|
||||
// Log level with color and bold
|
||||
var (levelText, levelColor) = GetLevelInfo(logEntry.LogLevel);
|
||||
textWriter.Write(levelColor);
|
||||
textWriter.Write(Bold);
|
||||
textWriter.Write(levelText);
|
||||
textWriter.Write(' ');
|
||||
|
||||
// Category
|
||||
textWriter.Write(CategoryColor);
|
||||
textWriter.Write(logEntry.Category);
|
||||
|
||||
// Message
|
||||
textWriter.Write(MessageColor);
|
||||
textWriter.Write(": ");
|
||||
textWriter.Write(message);
|
||||
|
||||
// Exception
|
||||
if (logEntry.Exception != null)
|
||||
{
|
||||
textWriter.Write(MessageColor);
|
||||
textWriter.WriteLine(logEntry.Exception.ToString());
|
||||
}
|
||||
else
|
||||
textWriter.WriteLine();
|
||||
}
|
||||
|
||||
private static (string text, string color) GetLevelInfo(LogLevel logLevel)
|
||||
{
|
||||
return logLevel switch
|
||||
{
|
||||
LogLevel.Critical => ("CRIT", CriticalColor),
|
||||
LogLevel.Error => ("ERRO", ErrorColor),
|
||||
LogLevel.Warning => ("WARN", WarningColor),
|
||||
LogLevel.Information => ("INFO", InfoColor),
|
||||
LogLevel.Debug => ("DEBG", DebugColor),
|
||||
LogLevel.Trace => ("TRCE", TraceColor),
|
||||
_ => ("NONE", "")
|
||||
};
|
||||
}
|
||||
}
|
||||
65
Moonlight.Api/Http/Controllers/AuthController.cs
Normal file
65
Moonlight.Api/Http/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private readonly IAuthenticationSchemeProvider SchemeProvider;
|
||||
|
||||
public AuthController(IAuthenticationSchemeProvider schemeProvider)
|
||||
{
|
||||
SchemeProvider = schemeProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<SchemeResponse[]>> GetSchemesAsync()
|
||||
{
|
||||
var schemes = await SchemeProvider.GetAllSchemesAsync();
|
||||
|
||||
return schemes
|
||||
.Where(scheme => !string.IsNullOrWhiteSpace(scheme.DisplayName))
|
||||
.Select(scheme => new SchemeResponse(scheme.Name, scheme.DisplayName!))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
[HttpGet("{schemeName:alpha}")]
|
||||
public async Task<ActionResult> StartAsync([FromRoute] string schemeName)
|
||||
{
|
||||
var scheme = await SchemeProvider.GetSchemeAsync(schemeName);
|
||||
|
||||
if (scheme == null || string.IsNullOrWhiteSpace(scheme.DisplayName))
|
||||
return Problem("Invalid authentication scheme name", statusCode: 400);
|
||||
|
||||
return Challenge(new AuthenticationProperties()
|
||||
{
|
||||
RedirectUri = "/"
|
||||
}, scheme.Name);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("claims")]
|
||||
public Task<ActionResult<ClaimResponse[]>> GetClaimsAsync()
|
||||
{
|
||||
var result = User.Claims
|
||||
.Select(claim => new ClaimResponse(claim.Type, claim.Value))
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult<ActionResult<ClaimResponse[]>>(result);
|
||||
}
|
||||
|
||||
[HttpGet("logout")]
|
||||
public Task<ActionResult> LogoutAsync()
|
||||
{
|
||||
return Task.FromResult<ActionResult>(
|
||||
SignOut(new AuthenticationProperties()
|
||||
{
|
||||
RedirectUri = "/"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
126
Moonlight.Api/Http/Controllers/UsersController.cs
Normal file
126
Moonlight.Api/Http/Controllers/UsersController.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Mappers;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/users")]
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<User> UserRepository;
|
||||
|
||||
public UsersController(DatabaseRepository<User> userRepository)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedData<UserResponse>>> GetAsync(
|
||||
[FromQuery] int startIndex,
|
||||
[FromQuery] int length,
|
||||
[FromQuery] FilterOptions? filterOptions
|
||||
)
|
||||
{
|
||||
// Validation
|
||||
if (startIndex < 0)
|
||||
return Problem("Invalid start index specified", statusCode: 400);
|
||||
|
||||
if (length is < 1 or > 100)
|
||||
return Problem("Invalid length specified");
|
||||
|
||||
var query = UserRepository
|
||||
.Query();
|
||||
|
||||
// Filters
|
||||
if (filterOptions != null)
|
||||
{
|
||||
foreach (var filterOption in filterOptions.Filters)
|
||||
{
|
||||
query = filterOption.Key switch
|
||||
{
|
||||
nameof(Database.Entities.User.Email) =>
|
||||
query.Where(user => EF.Functions.ILike(user.Email, $"%{filterOption.Value}%")),
|
||||
|
||||
nameof(Database.Entities.User.Username) =>
|
||||
query.Where(user => EF.Functions.ILike(user.Username, $"%{filterOption.Value}%")),
|
||||
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
var data = await query
|
||||
.ProjectToResponse()
|
||||
.Skip(startIndex)
|
||||
.Take(length)
|
||||
.ToArrayAsync();
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
return new PagedData<UserResponse>(data, total);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<ActionResult<UserResponse>> GetAsync([FromRoute] int id)
|
||||
{
|
||||
var user = await UserRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (user == null)
|
||||
return Problem("No user with this id found", statusCode: 404);
|
||||
|
||||
return UserMapper.MapToResponse(user);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<UserResponse>> CreateAsync([FromBody] CreateUserRequest request)
|
||||
{
|
||||
var user = UserMapper.MapToUser(request);
|
||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1);
|
||||
|
||||
var finalUser = await UserRepository.AddAsync(user);
|
||||
|
||||
return UserMapper.MapToResponse(finalUser);
|
||||
}
|
||||
|
||||
[HttpPatch("{id:int}")]
|
||||
public async Task<ActionResult<UserResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateUserRequest request)
|
||||
{
|
||||
var user = await UserRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (user == null)
|
||||
return Problem("No user with this id found", statusCode: 404);
|
||||
|
||||
UserMapper.Merge(user, request);
|
||||
await UserRepository.UpdateAsync(user);
|
||||
|
||||
return UserMapper.MapToResponse(user);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
||||
{
|
||||
var user = await UserRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(user => user.Id == id);
|
||||
|
||||
if (user == null)
|
||||
return Problem("No user with this id found", statusCode: 404);
|
||||
|
||||
await UserRepository.RemoveAsync(user);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
21
Moonlight.Api/Mappers/UserMapper.cs
Normal file
21
Moonlight.Api/Mappers/UserMapper.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
[Mapper]
|
||||
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||
public static partial class UserMapper
|
||||
{
|
||||
public static partial IQueryable<UserResponse> ProjectToResponse(this IQueryable<User> users);
|
||||
|
||||
public static partial UserResponse MapToResponse(User user);
|
||||
|
||||
public static partial void Merge([MappingTarget] User user, UpdateUserRequest request);
|
||||
|
||||
public static partial User MapToUser(CreateUserRequest request);
|
||||
}
|
||||
31
Moonlight.Api/Moonlight.Api.csproj
Normal file
31
Moonlight.Api/Moonlight.Api.csproj
Normal file
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Database\Migrations\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="@(Content)">
|
||||
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
14
Moonlight.Api/Properties/launchSettings.json
Normal file
14
Moonlight.Api/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5185",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Moonlight.Api/Services/DbMigrationService.cs
Normal file
49
Moonlight.Api/Services/DbMigrationService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moonlight.Api.Database;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
public class DbMigrationService : IHostedLifecycleService
|
||||
{
|
||||
private readonly ILogger<DbMigrationService> Logger;
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
|
||||
public DbMigrationService(ILogger<DbMigrationService> logger, IServiceProvider serviceProvider)
|
||||
{
|
||||
Logger = logger;
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task StartingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogTrace("Checking for pending migrations");
|
||||
|
||||
await using var scope = ServiceProvider.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken);
|
||||
var migrationNames = pendingMigrations.ToArray();
|
||||
|
||||
if (migrationNames.Length == 0)
|
||||
{
|
||||
Logger.LogDebug("No pending migrations found");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Pending migrations: {names}", string.Join(", ", migrationNames));
|
||||
Logger.LogInformation("Migration started");
|
||||
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Migration complete");
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
99
Moonlight.Api/Services/UserAuthService.cs
Normal file
99
Moonlight.Api/Services/UserAuthService.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
public class UserAuthService
|
||||
{
|
||||
private readonly DatabaseRepository<User> UserRepository;
|
||||
private readonly ILogger<UserAuthService> Logger;
|
||||
|
||||
private const string UserIdClaim = "UserId";
|
||||
private const string IssuedAtClaim = "IssuedAt";
|
||||
|
||||
public UserAuthService(DatabaseRepository<User> userRepository, ILogger<UserAuthService> logger)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||
{
|
||||
if (principal is null)
|
||||
return false;
|
||||
|
||||
var username = principal.FindFirstValue(ClaimTypes.Name);
|
||||
var email = principal.FindFirstValue(ClaimTypes.Email);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
Logger.LogWarning("Unable to sync user to database as name and/or email claims are missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We use email as the primary identifier here
|
||||
var user = await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(user => user.Email == email);
|
||||
|
||||
if (user == null) // Sync user if not already existing in the database
|
||||
{
|
||||
user = await UserRepository.AddAsync(new User()
|
||||
{
|
||||
Username = username,
|
||||
Email = email,
|
||||
InvalidateTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1)
|
||||
});
|
||||
}
|
||||
else // Update properties of existing user
|
||||
{
|
||||
user.Username = username;
|
||||
|
||||
await UserRepository.UpdateAsync(user);
|
||||
}
|
||||
|
||||
principal.Identities.First().AddClaims([
|
||||
new Claim(UserIdClaim, user.Id.ToString()),
|
||||
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
|
||||
{
|
||||
// Ignore malformed claims principal
|
||||
if (principal is not { Identity.IsAuthenticated: true })
|
||||
return false;
|
||||
|
||||
var userIdString = principal.FindFirstValue(UserIdClaim);
|
||||
|
||||
if (!int.TryParse(userIdString, out var userId))
|
||||
return false;
|
||||
|
||||
var user = await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(user => user.Id == userId);
|
||||
|
||||
if (user == null)
|
||||
return false;
|
||||
|
||||
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
||||
|
||||
if (!long.TryParse(issuedAtString, out var issuedAtUnix))
|
||||
return false;
|
||||
|
||||
var issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedAtUnix).ToUniversalTime();
|
||||
|
||||
// If the issued at timestamp is greater than the token validation timestamp
|
||||
// everything is fine. If not it means that the token should be invalidated
|
||||
// as it is too old
|
||||
|
||||
return issuedAt > user.InvalidateTimestamp;
|
||||
}
|
||||
}
|
||||
10
Moonlight.Api/Startup/IAppStartup.cs
Normal file
10
Moonlight.Api/Startup/IAppStartup.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public interface IAppStartup
|
||||
{
|
||||
public void PreBuild(WebApplicationBuilder builder);
|
||||
public void PostBuild(WebApplication application);
|
||||
public void PostMiddleware(WebApplication application);
|
||||
}
|
||||
91
Moonlight.Api/Startup/Startup.Auth.cs
Normal file
91
Moonlight.Api/Startup/Startup.Auth.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Services;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public partial class Startup
|
||||
{
|
||||
private static void AddAuth(WebApplicationBuilder builder)
|
||||
{
|
||||
var oidcOptions = new OidcOptions();
|
||||
builder.Configuration.GetSection("WebApp:Oidc").Bind(oidcOptions);
|
||||
|
||||
builder.Services.AddScoped<UserAuthService>();
|
||||
|
||||
builder.Services.AddAuthentication("Session")
|
||||
.AddCookie("Session", null, options =>
|
||||
{
|
||||
options.Events.OnSigningIn += async context =>
|
||||
{
|
||||
var authService = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<UserAuthService>();
|
||||
|
||||
var result = await authService.SyncAsync(context.Principal);
|
||||
|
||||
if (result)
|
||||
context.Properties.IsPersistent = true;
|
||||
else
|
||||
context.Principal = new ClaimsPrincipal();
|
||||
};
|
||||
|
||||
options.Events.OnValidatePrincipal += async context =>
|
||||
{
|
||||
var authService = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<UserAuthService>();
|
||||
|
||||
var result = await authService.ValidateAsync(context.Principal);
|
||||
|
||||
if (!result)
|
||||
context.RejectPrincipal();
|
||||
};
|
||||
|
||||
options.Cookie = new CookieBuilder()
|
||||
{
|
||||
Name = "token",
|
||||
Path = "/",
|
||||
IsEssential = true,
|
||||
SecurePolicy = CookieSecurePolicy.SameAsRequest
|
||||
};
|
||||
})
|
||||
.AddOpenIdConnect("OIDC", "OpenID Connect", options =>
|
||||
{
|
||||
options.Authority = oidcOptions.Authority;
|
||||
options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata;
|
||||
|
||||
var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
|
||||
|
||||
options.Scope.Clear();
|
||||
|
||||
foreach (var scope in scopes)
|
||||
options.Scope.Add(scope);
|
||||
|
||||
options.ResponseType = oidcOptions.ResponseType;
|
||||
options.ClientId = oidcOptions.ClientId;
|
||||
options.ClientSecret = oidcOptions.ClientSecret;
|
||||
|
||||
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
|
||||
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "preferred_username");
|
||||
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
|
||||
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
}
|
||||
|
||||
private static void UseAuth(WebApplication application)
|
||||
{
|
||||
application.UseAuthentication();
|
||||
application.UseAuthorization();
|
||||
}
|
||||
}
|
||||
36
Moonlight.Api/Startup/Startup.Base.cs
Normal file
36
Moonlight.Api/Startup/Startup.Base.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Moonlight.Shared.Http;
|
||||
using Moonlight.Api.Helpers;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public partial class Startup
|
||||
{
|
||||
private static void AddBase(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
});
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
||||
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
||||
}
|
||||
|
||||
private static void UseBase(WebApplication application)
|
||||
{
|
||||
|
||||
application.UseRouting();
|
||||
}
|
||||
|
||||
private static void MapBase(WebApplication application)
|
||||
{
|
||||
application.MapControllers();
|
||||
|
||||
application.MapFallbackToFile("index.html");
|
||||
}
|
||||
}
|
||||
19
Moonlight.Api/Startup/Startup.Database.cs
Normal file
19
Moonlight.Api/Startup/Startup.Database.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Services;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public partial class Startup
|
||||
{
|
||||
private static void AddDatabase(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddOptions<DatabaseOptions>().BindConfiguration("WebApp:Database");
|
||||
|
||||
builder.Services.AddDbContext<DataContext>();
|
||||
builder.Services.AddScoped(typeof(DatabaseRepository<>));
|
||||
builder.Services.AddHostedService<DbMigrationService>();
|
||||
}
|
||||
}
|
||||
24
Moonlight.Api/Startup/Startup.cs
Normal file
24
Moonlight.Api/Startup/Startup.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public partial class Startup : IAppStartup
|
||||
{
|
||||
public void PreBuild(WebApplicationBuilder builder)
|
||||
{
|
||||
AddBase(builder);
|
||||
AddAuth(builder);
|
||||
AddDatabase(builder);
|
||||
}
|
||||
|
||||
public void PostBuild(WebApplication application)
|
||||
{
|
||||
UseBase(application);
|
||||
UseAuth(application);
|
||||
}
|
||||
|
||||
public void PostMiddleware(WebApplication application)
|
||||
{
|
||||
MapBase(application);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user