Merge pull request #445 from Moonlight-Panel/v2.1_OptimizeNugetBuilding

Optimize nuget building. Refactored startup. Fixed smaller issues
This commit is contained in:
2025-07-14 21:09:21 +02:00
committed by GitHub
58 changed files with 969 additions and 1018 deletions

View File

@@ -1,30 +0,0 @@
**/.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
**/storage
**/compose.yml
LICENSE
README.md
# For the people who run moonlight inside the main repo with the relative data path
data

View File

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

View File

@@ -1,24 +1,45 @@
using MoonCore.Helpers;
using YamlDotNet.Serialization;
namespace Moonlight.ApiServer.Configuration;
public class AppConfiguration
public record AppConfiguration
{
[YamlMember(Description = "The public url your instance should be accessible through")]
public string PublicUrl { get; set; } = "http://localhost:5165";
[YamlMember(Description = "The credentials of the postgres which moonlight should use")]
public DatabaseConfig Database { get; set; } = new();
[YamlMember(Description = "Settings regarding authentication")]
public AuthenticationConfig Authentication { get; set; } = new();
[YamlMember(Description = "These options are only meant for development purposes")]
public DevelopmentConfig Development { get; set; } = new();
public ClientConfig Client { get; set; } = new();
public FrontendData Frontend { get; set; } = new();
public KestrelConfig Kestrel { get; set; } = new();
public MetricsData Metrics { get; set; } = new();
public class ClientConfig
public static AppConfiguration CreateEmpty()
{
public bool Enable { get; set; } = true;
return new AppConfiguration()
{
// Set arrays as empty here
Kestrel = new()
{
AllowedOrigins = []
}
};
}
public record FrontendData
{
[YamlMember(Description = "Enable the hosting of the frontend. Disable this if you only want to run the api server")]
public bool EnableHosting { get; set; } = true;
}
public class DatabaseConfig
public record DatabaseConfig
{
public string Host { get; set; } = "your-database-host.name";
public int Port { get; set; } = 5432;
@@ -29,15 +50,19 @@ public class AppConfiguration
public string Database { get; set; } = "db_name";
}
public class AuthenticationConfig
public record AuthenticationConfig
{
[YamlMember(Description = "The secret token to use for creating jwts and encrypting things. This needs to be at least 32 characters long")]
public string Secret { get; set; } = Formatter.GenerateString(32);
[YamlMember(Description = "The lifespan of generated user tokens in hours")]
public int TokenDuration { get; set; } = 24 * 10;
[YamlMember(Description = "This enables the use of the local oauth2 provider, so moonlight will use itself as an oauth2 provider")]
public bool EnableLocalOAuth2 { get; set; } = true;
public OAuth2Data OAuth2 { get; set; } = new();
public class OAuth2Data
public record OAuth2Data
{
public string Secret { get; set; } = Formatter.GenerateString(32);
public string ClientId { get; set; } = Formatter.GenerateString(8);
@@ -46,24 +71,32 @@ public class AppConfiguration
public string? AccessEndpoint { get; set; }
public string? AuthorizationRedirect { get; set; }
[YamlMember(Description = "This specifies if the first registered user will become an admin automatically. This only works when using local oauth2")]
public bool FirstUserAdmin { get; set; } = true;
}
}
public class DevelopmentConfig
public record DevelopmentConfig
{
[YamlMember(Description = "This toggles the availability of the api docs via /api/swagger")]
public bool EnableApiDocs { get; set; } = false;
}
public class KestrelConfig
public record KestrelConfig
{
[YamlMember(Description = "The upload limit in megabytes for the api server")]
public int UploadLimit { get; set; } = 100;
public string AllowedOrigins { get; set; } = "*";
[YamlMember(Description = "The allowed origins for the api server. Use * to allow all origins (which is not advised)")]
public string[] AllowedOrigins { get; set; } = ["*"];
}
public class MetricsData
public record MetricsData
{
[YamlMember(Description = "This enables the collecting of metrics and allows access to the /metrics endpoint")]
public bool Enable { get; set; } = false;
[YamlMember(Description = "The interval in which metrics are created, specified in seconds")]
public int Interval { get; set; } = 15;
}
}

View File

@@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Database.Entities;
public class ApiKey
{

View File

@@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Database.Entities;
public class User
{

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moonlight.ApiServer.Services;

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Requests.Admin.Sys;

View File

@@ -1,7 +1,6 @@
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Requests.Admin.Sys;
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;

View File

@@ -1,19 +1,17 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Interfaces;
using Moonlight.Shared.Http.Requests.Auth;
using Moonlight.Shared.Http.Responses.Auth;
using Moonlight.Shared.Http.Responses.OAuth2;
namespace Moonlight.ApiServer.Http.Controllers.Auth;
@@ -76,7 +74,7 @@ public class AuthController : Controller
// Generate token
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
Expires = DateTime.Now.AddYears(Configuration.Authentication.TokenDuration),
Expires = DateTime.Now.AddHours(Configuration.Authentication.TokenDuration),
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now.AddMinutes(-1),
Claims = new Dictionary<string, object>()

View File

@@ -1,4 +1,5 @@
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Misc;

View File

@@ -14,8 +14,8 @@
}
<link href="manifest.webmanifest" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="/img/icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/img/icon-192.png" />
<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" />
</head>
<body>

View File

@@ -4,6 +4,7 @@ using System.Security.Claims;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
@@ -55,11 +56,11 @@ public partial class OAuth2Controller : Controller
throw new HttpApiException("Invalid oauth2 request", 400);
}
Response.StatusCode = 200;
string html;
if (view == "register")
{
var html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
@@ -70,7 +71,7 @@ public partial class OAuth2Controller : Controller
}
else
{
var html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
@@ -79,6 +80,10 @@ public partial class OAuth2Controller : Controller
await Response.WriteAsync(html);
}
await Results
.Text(html, "text/html")
.ExecuteAsync(HttpContext);
}
[AllowAnonymous]
@@ -116,7 +121,6 @@ public partial class OAuth2Controller : Controller
var code = await GenerateCode(user);
Response.Redirect($"{redirectUri}?code={code}");
return;
}
else
{
@@ -124,39 +128,38 @@ public partial class OAuth2Controller : Controller
var code = await GenerateCode(user);
Response.Redirect($"{redirectUri}?code={code}");
return;
}
}
catch (HttpApiException e)
{
errorMessage = e.Title;
}
Response.StatusCode = 200;
if (view == "register")
{
var html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
string html;
if (view == "register")
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
parameters.Add("ResponseType", responseType);
parameters.Add("ErrorMessage", errorMessage!);
});
await Response.WriteAsync(html);
}
else
{
var html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
parameters.Add("ResponseType", responseType);
parameters.Add("ErrorMessage", errorMessage!);
});
}
else
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
parameters.Add("ResponseType", responseType);
parameters.Add("ErrorMessage", errorMessage!);
});
html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
{
parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri);
parameters.Add("ResponseType", responseType);
parameters.Add("ErrorMessage", errorMessage!);
});
}
await Response.WriteAsync(html);
await Results
.Text(html, "text/html")
.ExecuteAsync(HttpContext);
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.Metrics;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Services;

View File

@@ -1,5 +1,6 @@
using System.Diagnostics.Metrics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Interfaces;

View File

@@ -1,3 +1,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database;
@@ -8,7 +12,6 @@ using Moonlight.ApiServer.Models;
using Moonlight.ApiServer.Plugins;
using Moonlight.ApiServer.Services;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Moonlight.ApiServer.Implementations.Startup;
@@ -83,7 +86,7 @@ public class CoreStartup : IPluginStartup
#region Client / Frontend
if (configuration.Client.Enable)
if (configuration.Frontend.EnableHosting)
{
builder.Services.AddSingleton(new FrontendConfigurationOption()
{

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
@@ -12,15 +12,9 @@
<Folder Include="Database\Migrations\" />
<Folder Include="Helpers\" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
<Pack>false</Pack>
</Content>
</ItemGroup>
<PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.2</Version>
<Version>2.1.3</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -32,7 +26,6 @@
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20" />
<PackageReference Include="Hangfire.Core" Version="1.8.20" />
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
<PackageReference Include="MoonCore" Version="1.9.2" />
<PackageReference Include="MoonCore.Extended" Version="1.3.5" />
@@ -45,16 +38,6 @@
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="**\*.razor" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />

View File

@@ -1,3 +1,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
namespace Moonlight.ApiServer.Plugins;
public interface IPluginStartup

View File

@@ -1,5 +0,0 @@
using Moonlight.ApiServer;
var startup = new Startup();
await startup.Run(args);

View File

@@ -1,6 +1,5 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Attributes;
using Moonlight.ApiServer.Configuration;

View File

@@ -1,5 +1,7 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MoonCore.Attributes;
using MoonCore.Helpers;

View File

@@ -1,5 +1,6 @@
using Moonlight.ApiServer.Interfaces;
using System.IO.Compression;
using Microsoft.Extensions.Logging;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using Moonlight.Shared.Http.Responses.Admin.Sys;

View File

@@ -1,6 +1,7 @@
using System.IO.Compression;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
using MoonCore.Attributes;
using MoonCore.Exceptions;
@@ -82,7 +83,7 @@ public class FrontendService
public async Task<Stream> GenerateZip() // TODO: Rework to be able to extract everything successfully
{
// We only allow the access to this function when we are actually hosting the frontend
if (!Configuration.Client.Enable)
if (!Configuration.Frontend.EnableHosting)
throw new HttpApiException("The hosting of the wasm client has been disabled", 400);
// Load and check wasm path

View File

@@ -1,4 +1,7 @@
using System.Diagnostics.Metrics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Interfaces;

View File

@@ -1,547 +0,0 @@
using System.Text;
using System.Text.Json;
using Hangfire;
using Hangfire.EntityFrameworkCore;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using MoonCore.EnvConfiguration;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Extensions;
using MoonCore.Extended.Helpers;
using MoonCore.Extended.JwtInvalidation;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.Logging;
using MoonCore.Permissions;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Implementations;
using Moonlight.ApiServer.Implementations.Startup;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Plugins;
namespace Moonlight.ApiServer;
// Cry about it
#pragma warning disable ASP0000
public class Startup
{
private string[] Args;
// Logging
private ILoggerProvider[] LoggerProviders;
private ILoggerFactory LoggerFactory;
private ILogger<Startup> Logger;
// Configuration
private AppConfiguration Configuration;
private IConfigurationRoot ConfigurationRoot;
// WebApplication Stuff
private WebApplication WebApplication;
private WebApplicationBuilder WebApplicationBuilder;
// Plugin Loading
private IPluginStartup[] PluginStartups;
private IPluginStartup[] AdditionalPlugins;
private IServiceProvider PluginLoadServiceProvider;
public async Task Run(string[] args, IPluginStartup[]? additionalPlugins = null)
{
Args = args;
AdditionalPlugins = additionalPlugins ?? [];
await PrintVersion();
await CreateStorage();
await SetupAppConfiguration();
await SetupLogging();
await InitializePlugins();
await CreateWebApplicationBuilder();
await ConfigureKestrel();
await RegisterAppConfiguration();
await RegisterLogging();
await RegisterBase();
await RegisterDatabase();
await RegisterAuth();
await RegisterCors();
await RegisterHangfire();
await HookPluginBuild();
await RegisterPluginAssets();
await BuildWebApplication();
await PrepareDatabase();
await UseCors();
await UseBase();
await UseAuth();
await UseHangfire();
await HookPluginConfigure();
await MapBase();
await HookPluginEndpoints();
await WebApplication.RunAsync();
}
private Task PrintVersion()
{
// Fancy start console output... yes very fancy :>
var rainbow = new Crayon.Rainbow(0.5);
foreach (var c in "Moonlight")
{
Console.Write(
rainbow
.Next()
.Bold()
.Text(c.ToString())
);
}
Console.WriteLine();
return Task.CompletedTask;
}
private Task CreateStorage()
{
Directory.CreateDirectory("storage");
Directory.CreateDirectory(Path.Combine("storage", "logs"));
return Task.CompletedTask;
}
#region Base
private Task RegisterBase()
{
WebApplicationBuilder.Services.AutoAddServices<Startup>();
WebApplicationBuilder.Services.AddHttpClient();
WebApplicationBuilder.Services.AddApiExceptionHandler();
// Add pre-existing services
WebApplicationBuilder.Services.AddSingleton(Configuration);
// Configure controllers
var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
// Add plugin assemblies as application parts
foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct())
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
return Task.CompletedTask;
}
private Task UseBase()
{
WebApplication.UseRouting();
WebApplication.UseExceptionHandler();
if (Configuration.Client.Enable)
{
if (WebApplication.Environment.IsDevelopment())
WebApplication.UseWebAssemblyDebugging();
WebApplication.UseBlazorFrameworkFiles();
WebApplication.UseStaticFiles();
}
return Task.CompletedTask;
}
private Task MapBase()
{
WebApplication.MapControllers();
if (Configuration.Client.Enable)
WebApplication.MapFallbackToController("Index", "Frontend");
return Task.CompletedTask;
}
private Task ConfigureKestrel()
{
WebApplicationBuilder.WebHost.ConfigureKestrel(kestrelOptions =>
{
var maxUploadInBytes = ByteConverter
.FromMegaBytes(Configuration.Kestrel.UploadLimit)
.Bytes;
kestrelOptions.Limits.MaxRequestBodySize = maxUploadInBytes;
});
return Task.CompletedTask;
}
#endregion
#region Plugin Loading
private Task InitializePlugins()
{
// Create service provider for starting up
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(Configuration);
serviceCollection.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddAnsiConsole();
});
PluginLoadServiceProvider = serviceCollection.BuildServiceProvider();
// Collect startups
var pluginStartups = new List<IPluginStartup>();
pluginStartups.AddRange(AdditionalPlugins); // Used by the development server
// Do NOT remove the following comment, as its used to place the plugin startup register calls
// MLBUILD_PLUGIN_STARTUP_HERE
PluginStartups = pluginStartups.ToArray();
return Task.CompletedTask;
}
private Task RegisterPluginAssets()
{
return Task.CompletedTask;
}
#region Hooks
private async Task HookPluginBuild()
{
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'BuildApp' for '{name}': {e}",
pluginAppStartup.GetType().FullName,
e
);
}
}
}
private async Task HookPluginConfigure()
{
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'ConfigureApp' for '{name}': {e}",
pluginAppStartup.GetType().FullName,
e
);
}
}
}
private async Task HookPluginEndpoints()
{
foreach (var pluginEndpointStartup in PluginStartups)
{
try
{
await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'ConfigureEndpoints' for '{name}': {e}",
pluginEndpointStartup.GetType().FullName,
e
);
}
}
}
#endregion
#endregion
#region Configurations
private async Task SetupAppConfiguration()
{
// Configure configuration (wow)
var configurationBuilder = new ConfigurationBuilder();
// Ensure configuration file exists
var jsonFilePath = Path.Combine(Directory.GetCurrentDirectory(), "storage", "app.json");
if (!File.Exists(jsonFilePath))
await File.WriteAllTextAsync(jsonFilePath, JsonSerializer.Serialize(new AppConfiguration()));
configurationBuilder.AddJsonFile(
jsonFilePath
);
configurationBuilder.AddEnvironmentVariables(prefix: "MOONLIGHT_", separator: "_");
ConfigurationRoot = configurationBuilder.Build();
// Retrieve configuration
Configuration = ConfigurationRoot.Get<AppConfiguration>()!;
}
private Task RegisterAppConfiguration()
{
WebApplicationBuilder.Services.AddSingleton(Configuration);
return Task.CompletedTask;
}
#endregion
#region Web Application
private Task CreateWebApplicationBuilder()
{
WebApplicationBuilder = WebApplication.CreateBuilder(Args);
return Task.CompletedTask;
}
private Task BuildWebApplication()
{
WebApplication = WebApplicationBuilder.Build();
return Task.CompletedTask;
}
#endregion
#region Logging
private Task SetupLogging()
{
LoggerFactory = new LoggerFactory();
LoggerFactory.AddAnsiConsole();
Logger = LoggerFactory.CreateLogger<Startup>();
return Task.CompletedTask;
}
private async Task RegisterLogging()
{
// Configure application logging
WebApplicationBuilder.Logging.ClearProviders();
WebApplicationBuilder.Logging.AddAnsiConsole();
WebApplicationBuilder.Logging.AddFile(Path.Combine("storage", "logs", "moonlight.log"));
// Logging levels
var logConfigPath = Path.Combine("storage", "logConfig.json");
// Ensure logging config, add a default one is missing
if (!File.Exists(logConfigPath))
{
var defaultLogLevels = new Dictionary<string, string>
{
{ "Default", "Information" },
{ "Microsoft.AspNetCore", "Warning" },
{ "System.Net.Http.HttpClient", "Warning" }
};
var logLevelsJson = JsonSerializer.Serialize(defaultLogLevels);
await File.WriteAllTextAsync(logConfigPath, logLevelsJson);
}
// Add logging configuration
var logLevels = JsonSerializer.Deserialize<Dictionary<string, string>>(
await File.ReadAllTextAsync(logConfigPath)
)!;
foreach (var level in logLevels)
WebApplicationBuilder.Logging.AddFilter(level.Key, Enum.Parse<LogLevel>(level.Value));
// Mute exception handler middleware
// https://github.com/dotnet/aspnetcore/issues/19740
WebApplicationBuilder.Logging.AddFilter(
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
LogLevel.Critical
);
WebApplicationBuilder.Logging.AddFilter(
"Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware",
LogLevel.Critical
);
}
#endregion
#region Database
private Task RegisterDatabase()
{
WebApplicationBuilder.Services.AddDatabaseMappings();
WebApplicationBuilder.Services.AddServiceCollectionAccessor();
WebApplicationBuilder.Services.AddScoped(typeof(DatabaseRepository<>));
return Task.CompletedTask;
}
private async Task PrepareDatabase()
{
await WebApplication.Services.EnsureDatabaseMigrated();
WebApplication.Services.GenerateDatabaseMappings();
}
#endregion
#region Authentication & Authorisation
private Task RegisterAuth()
{
WebApplicationBuilder.Services
.AddAuthentication("coreAuthentication")
.AddJwtBearer("coreAuthentication", options =>
{
options.TokenValidationParameters = new()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
Configuration.Authentication.Secret
)),
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidateAudience = true,
ValidAudience = Configuration.PublicUrl,
ValidateIssuer = true,
ValidIssuer = Configuration.PublicUrl
};
});
WebApplicationBuilder.Services.AddJwtBearerInvalidation("coreAuthentication");
WebApplicationBuilder.Services.AddScoped<IJwtInvalidateHandler, UserAuthInvalidation>();
WebApplicationBuilder.Services.AddAuthorization();
WebApplicationBuilder.Services.AddAuthorizationPermissions(options =>
{
options.ClaimName = "permissions";
options.Prefix = "permissions:";
});
// Add local oauth2 provider if enabled
if (Configuration.Authentication.EnableLocalOAuth2)
WebApplicationBuilder.Services.AddScoped<IOAuth2Provider, LocalOAuth2Provider>();
return Task.CompletedTask;
}
private Task UseAuth()
{
WebApplication.UseAuthentication();
WebApplication.UseAuthorization();
return Task.CompletedTask;
}
#endregion
#region Cors
private Task RegisterCors()
{
var allowedOrigins = Configuration.Kestrel.AllowedOrigins.Split(";", StringSplitOptions.RemoveEmptyEntries);
WebApplicationBuilder.Services.AddCors(options =>
{
var cors = new CorsPolicyBuilder();
if (allowedOrigins.Contains("*"))
{
cors.SetIsOriginAllowed(_ => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}
else
{
cors.WithOrigins(allowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
}
options.AddDefaultPolicy(
cors.Build()
);
});
return Task.CompletedTask;
}
private Task UseCors()
{
WebApplication.UseCors();
return Task.CompletedTask;
}
#endregion
#region Hangfire
private Task RegisterHangfire()
{
WebApplicationBuilder.Services.AddHangfire((provider, configuration) =>
{
configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
configuration.UseSimpleAssemblyNameTypeSerializer();
configuration.UseRecommendedSerializerSettings();
configuration.UseEFCoreStorage(() =>
{
var scope = provider.CreateScope();
return scope.ServiceProvider.GetRequiredService<CoreDataContext>();
}, new EFCoreStorageOptions());
});
WebApplicationBuilder.Services.AddHangfireServer();
WebApplicationBuilder.Logging.AddFilter(
"Hangfire.Server.BackgroundServerProcess",
LogLevel.Warning
);
WebApplicationBuilder.Logging.AddFilter(
"Hangfire.BackgroundJobServer",
LogLevel.Warning
);
return Task.CompletedTask;
}
private Task UseHangfire()
{
if (WebApplication.Environment.IsDevelopment())
WebApplication.UseHangfireDashboard();
return Task.CompletedTask;
}
#endregion
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
using Hangfire;
using Hangfire.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moonlight.ApiServer.Database;
namespace Moonlight.ApiServer.Startup;
public partial class Startup
{
private Task RegisterHangfire()
{
WebApplicationBuilder.Services.AddHangfire((provider, configuration) =>
{
configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
configuration.UseSimpleAssemblyNameTypeSerializer();
configuration.UseRecommendedSerializerSettings();
configuration.UseEFCoreStorage(() =>
{
var scope = provider.CreateScope();
return scope.ServiceProvider.GetRequiredService<CoreDataContext>();
}, new EFCoreStorageOptions());
});
WebApplicationBuilder.Services.AddHangfireServer();
WebApplicationBuilder.Logging.AddFilter(
"Hangfire.Server.BackgroundServerProcess",
LogLevel.Warning
);
WebApplicationBuilder.Logging.AddFilter(
"Hangfire.BackgroundJobServer",
LogLevel.Warning
);
return Task.CompletedTask;
}
private Task UseHangfire()
{
if (WebApplication.Environment.IsDevelopment())
WebApplication.UseHangfireDashboard();
return Task.CompletedTask;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,6 +64,7 @@
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\js\moonCore.js" />
<_ContentIncludedByDefault Remove="wwwroot\js\moonlight.js" />
<_ContentIncludedByDefault Remove="wwwroot\svg\logo.svg" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,20 @@
using Moonlight.Client;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Moonlight.Client.Runtime;
var startup = new Startup();
using Moonlight.Client.Startup;
var pluginLoader = new PluginLoader();
pluginLoader.Initialize();
await startup.Run(args, pluginLoader.Instances);
var startup = new Startup();
await startup.Initialize(pluginLoader.Instances);
var wasmHostBuilder = WebAssemblyHostBuilder.CreateDefault(args);
await startup.AddMoonlight(wasmHostBuilder);
var wasmApp = wasmHostBuilder.Build();
await startup.AddMoonlight(wasmApp);
await wasmApp.RunAsync();

View File

@@ -1,16 +0,0 @@
{
"apiUrl": "http://localhost:5165",
"hostEnvironment": "Static",
"theme": {
"variables": {
}
},
"scripts": [
],
"plugins": {
"assemblies": [
],
"entrypoints": [
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

View File

@@ -12,7 +12,7 @@
<PropertyGroup>
<PackageTags>frontend</PackageTags>
<PackageId>Moonlight.Client</PackageId>
<Version>2.1.2</Version>
<Version>2.1.3</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the client for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -27,16 +27,6 @@
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.7" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="Styles\**\*" Exclude="storage\**\*;bin\**\*;obj\**\*;Styles\node_modules\**\*">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />

View File

@@ -1,310 +0,0 @@
using System.Text.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Blazor.FlyonUi;
using MoonCore.Blazor.FlyonUi.Auth;
using MoonCore.Blazor.Services;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.Logging;
using MoonCore.Permissions;
using Moonlight.Client.Plugins;
using Moonlight.Client.Services;
using Moonlight.Shared.Misc;
using Moonlight.Client.UI;
using WindowService = Moonlight.Client.Services.WindowService;
namespace Moonlight.Client;
public class Startup
{
private string[] Args;
// Configuration
private FrontendConfiguration Configuration;
// Logging
private ILoggerFactory LoggerFactory;
private ILogger<Startup> Logger;
// WebAssemblyHost
private WebAssemblyHostBuilder WebAssemblyHostBuilder;
private WebAssemblyHost WebAssemblyHost;
// Plugin Loading
private IPluginStartup[] AdditionalPlugins;
private IPluginStartup[] PluginStartups;
private IServiceProvider PluginLoadServiceProvider;
public async Task Run(string[] args, IPluginStartup[]? additionalPlugins = null)
{
Args = args;
AdditionalPlugins = additionalPlugins ?? [];
await PrintVersion();
await SetupLogging();
await CreateWebAssemblyHostBuilder();
await LoadConfiguration();
await InitializePlugins();
await RegisterLogging();
await RegisterBase();
await RegisterAuthentication();
await HookPluginBuild();
await BuildWebAssemblyHost();
await HookPluginConfigure();
await LoadAssets();
await WebAssemblyHost.RunAsync();
}
private Task PrintVersion()
{
// Fancy start console output... yes very fancy :>
Console.Write("Running ");
var rainbow = new Crayon.Rainbow(0.5);
foreach (var c in "Moonlight")
{
Console.Write(
rainbow
.Next()
.Bold()
.Text(c.ToString())
);
}
Console.WriteLine();
return Task.CompletedTask;
}
private async Task LoadConfiguration()
{
try
{
using var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(WebAssemblyHostBuilder.HostEnvironment.BaseAddress);
var jsonText = await httpClient.GetStringAsync("frontend.json");
Configuration = JsonSerializer.Deserialize<FrontendConfiguration>(jsonText, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
})!;
WebAssemblyHostBuilder.Services.AddSingleton(Configuration);
}
catch (Exception e)
{
Logger.LogCritical("Unable to load configuration. Unable to continue: {e}", e);
throw;
}
}
private Task RegisterBase()
{
WebAssemblyHostBuilder.RootComponents.Add<App>("#app");
WebAssemblyHostBuilder.RootComponents.Add<HeadOutlet>("head::after");
WebAssemblyHostBuilder.Services.AddScoped(_ =>
new HttpClient
{
BaseAddress = new Uri(Configuration.ApiUrl)
}
);
WebAssemblyHostBuilder.Services.AddScoped(sp =>
{
var httpClient = sp.GetRequiredService<HttpClient>();
var httpApiClient = new HttpApiClient(httpClient);
var localStorageService = sp.GetRequiredService<LocalStorageService>();
httpApiClient.OnConfigureRequest += async request =>
{
var accessToken = await localStorageService.GetString("AccessToken");
if (string.IsNullOrEmpty(accessToken))
return;
request.Headers.Add("Authorization", $"Bearer {accessToken}");
};
return httpApiClient;
});
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
WebAssemblyHostBuilder.Services.AddFileManagerOperations();
WebAssemblyHostBuilder.Services.AddFlyonUiServices();
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
WebAssemblyHostBuilder.Services.AddScoped<ThemeService>();
WebAssemblyHostBuilder.Services.AutoAddServices<Startup>();
return Task.CompletedTask;
}
#region Asset Loading
private async Task LoadAssets()
{
var jsRuntime = WebAssemblyHost.Services.GetRequiredService<IJSRuntime>();
foreach (var scriptName in Configuration.Scripts)
await jsRuntime.InvokeVoidAsync("moonlight.assets.loadJavascript", scriptName);
foreach (var styleName in Configuration.Styles)
await jsRuntime.InvokeVoidAsync("moonlight.assets.loadStylesheet", styleName);
}
#endregion
#region Plugins
private Task InitializePlugins()
{
// Define minimal service collection
var startupSc = new ServiceCollection();
// Create logging proxy
startupSc.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddAnsiConsole();
});
PluginLoadServiceProvider = startupSc.BuildServiceProvider();
// Collect startups
var pluginStartups = new List<IPluginStartup>();
pluginStartups.AddRange(AdditionalPlugins); // Used by the development server
// Do NOT remove the following comment, as its used to place the plugin startup register calls
// MLBUILD_PLUGIN_STARTUP_HERE
PluginStartups = pluginStartups.ToArray();
// Add application assembly service
var appAssemblyService = new ApplicationAssemblyService();
appAssemblyService.Assemblies.AddRange(
PluginStartups
.Select(x => x.GetType().Assembly)
.Distinct()
);
WebAssemblyHostBuilder.Services.AddSingleton(appAssemblyService);
return Task.CompletedTask;
}
#region Hooks
private async Task HookPluginBuild()
{
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebAssemblyHostBuilder);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'BuildApp' for '{name}': {e}",
pluginAppStartup.GetType().FullName,
e
);
}
}
}
private async Task HookPluginConfigure()
{
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebAssemblyHost);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'ConfigureApp' for '{name}': {e}",
pluginAppStartup.GetType().FullName,
e
);
}
}
}
#endregion
#endregion
#region Logging
private Task SetupLogging()
{
LoggerFactory = new LoggerFactory();
LoggerFactory.AddAnsiConsole();
Logger = LoggerFactory.CreateLogger<Startup>();
return Task.CompletedTask;
}
private Task RegisterLogging()
{
WebAssemblyHostBuilder.Logging.ClearProviders();
WebAssemblyHostBuilder.Logging.AddAnsiConsole();
return Task.CompletedTask;
}
#endregion
#region Web Application
private Task CreateWebAssemblyHostBuilder()
{
WebAssemblyHostBuilder = WebAssemblyHostBuilder.CreateDefault(Args);
return Task.CompletedTask;
}
private Task BuildWebAssemblyHost()
{
WebAssemblyHost = WebAssemblyHostBuilder.Build();
return Task.CompletedTask;
}
#endregion
#region Authentication
private Task RegisterAuthentication()
{
WebAssemblyHostBuilder.Services.AddAuthorizationCore();
WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState();
WebAssemblyHostBuilder.Services.AddAuthenticationStateManager<RemoteAuthStateManager>();
WebAssemblyHostBuilder.Services.AddAuthorizationPermissions(options =>
{
options.ClaimName = "permissions";
options.Prefix = "permissions:";
});
return Task.CompletedTask;
}
#endregion
}

View File

@@ -0,0 +1,25 @@
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Blazor.FlyonUi.Auth;
using MoonCore.Permissions;
using Moonlight.Client.Services;
namespace Moonlight.Client.Startup;
public partial class Startup
{
private Task RegisterAuthentication()
{
WebAssemblyHostBuilder.Services.AddAuthorizationCore();
WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState();
WebAssemblyHostBuilder.Services.AddAuthenticationStateManager<RemoteAuthStateManager>();
WebAssemblyHostBuilder.Services.AddAuthorizationPermissions(options =>
{
options.ClaimName = "permissions";
options.Prefix = "permissions:";
});
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Blazor.FlyonUi;
using MoonCore.Blazor.Services;
using MoonCore.Extensions;
using MoonCore.Helpers;
using Moonlight.Client.Services;
using Moonlight.Client.UI;
namespace Moonlight.Client.Startup;
public partial class Startup
{
private Task RegisterBase()
{
WebAssemblyHostBuilder.RootComponents.Add<App>("#app");
WebAssemblyHostBuilder.RootComponents.Add<HeadOutlet>("head::after");
WebAssemblyHostBuilder.Services.AddScoped(_ =>
new HttpClient
{
BaseAddress = new Uri(Configuration.ApiUrl)
}
);
WebAssemblyHostBuilder.Services.AddScoped(sp =>
{
var httpClient = sp.GetRequiredService<HttpClient>();
var httpApiClient = new HttpApiClient(httpClient);
var localStorageService = sp.GetRequiredService<LocalStorageService>();
httpApiClient.OnConfigureRequest += async request =>
{
var accessToken = await localStorageService.GetString("AccessToken");
if (string.IsNullOrEmpty(accessToken))
return;
request.Headers.Add("Authorization", $"Bearer {accessToken}");
};
return httpApiClient;
});
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
WebAssemblyHostBuilder.Services.AddFileManagerOperations();
WebAssemblyHostBuilder.Services.AddFlyonUiServices();
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
WebAssemblyHostBuilder.Services.AddScoped<ThemeService>();
WebAssemblyHostBuilder.Services.AutoAddServices<Startup>();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,24 @@
using MoonCore.Logging;
namespace Moonlight.Client.Startup;
public partial class Startup
{
private Task SetupLogging()
{
var loggerFactory = new LoggerFactory();
loggerFactory.AddAnsiConsole();
Logger = loggerFactory.CreateLogger<Startup>();
return Task.CompletedTask;
}
private Task RegisterLogging()
{
WebAssemblyHostBuilder.Logging.ClearProviders();
WebAssemblyHostBuilder.Logging.AddAnsiConsole();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,52 @@
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.Shared.Misc;
namespace Moonlight.Client.Startup;
public partial class Startup
{
private Task PrintVersion()
{
// Fancy start console output... yes very fancy :>
Console.Write("Running ");
var rainbow = new Crayon.Rainbow(0.5);
foreach (var c in "Moonlight")
{
Console.Write(
rainbow
.Next()
.Bold()
.Text(c.ToString())
);
}
Console.WriteLine();
return Task.CompletedTask;
}
private async Task LoadConfiguration()
{
try
{
using var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(WebAssemblyHostBuilder.HostEnvironment.BaseAddress);
var jsonText = await httpClient.GetStringAsync("frontend.json");
Configuration = JsonSerializer.Deserialize<FrontendConfiguration>(jsonText, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
})!;
WebAssemblyHostBuilder.Services.AddSingleton(Configuration);
}
catch (Exception e)
{
Logger.LogCritical("Unable to load configuration. Unable to continue: {e}", e);
throw;
}
}
}

View File

@@ -0,0 +1,78 @@
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Logging;
using Moonlight.Client.Plugins;
using Moonlight.Client.Services;
namespace Moonlight.Client.Startup;
public partial class Startup
{
private IPluginStartup[] PluginStartups;
private IServiceProvider PluginLoadServiceProvider;
private Task InitializePlugins()
{
// Define minimal service collection
var startupSc = new ServiceCollection();
// Create logging proxy
startupSc.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddAnsiConsole();
});
PluginLoadServiceProvider = startupSc.BuildServiceProvider();
// Add application assembly service
var appAssemblyService = new ApplicationAssemblyService();
appAssemblyService.Assemblies.AddRange(
PluginStartups
.Select(x => x.GetType().Assembly)
.Distinct()
);
WebAssemblyHostBuilder.Services.AddSingleton(appAssemblyService);
return Task.CompletedTask;
}
private async Task HookPluginBuild()
{
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebAssemblyHostBuilder);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'BuildApp' for '{name}': {e}",
pluginAppStartup.GetType().FullName,
e
);
}
}
}
private async Task HookPluginConfigure()
{
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebAssemblyHost);
}
catch (Exception e)
{
Logger.LogError(
"An error occured while processing 'ConfigureApp' for '{name}': {e}",
pluginAppStartup.GetType().FullName,
e
);
}
}
}
}

View File

@@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Moonlight.Client.Plugins;
using Moonlight.Shared.Misc;
namespace Moonlight.Client.Startup;
public partial class Startup
{
public ILogger<Startup> Logger { get; private set; }
// WebAssemblyHost
public WebAssemblyHostBuilder WebAssemblyHostBuilder { get; private set; }
public WebAssemblyHost WebAssemblyHost { get; private set; }
// Configuration
public FrontendConfiguration Configuration { get; private set; }
public Task Initialize(IPluginStartup[]? plugins = null)
{
PluginStartups = plugins ?? [];
return Task.CompletedTask;
}
public async Task AddMoonlight(WebAssemblyHostBuilder builder)
{
WebAssemblyHostBuilder = builder;
await PrintVersion();
await SetupLogging();
await LoadConfiguration();
await InitializePlugins();
await RegisterLogging();
await RegisterBase();
await RegisterAuthentication();
await HookPluginBuild();
}
public async Task AddMoonlight(WebAssemblyHost assemblyHost)
{
WebAssemblyHost = assemblyHost;
await HookPluginConfigure();
}
}

View File

@@ -23,7 +23,7 @@
class="inline-grid shrink-0 align-middle">
<img
class="h-8 rounded-full"
src="/svg/logo.svg"
src="/_content/Moonlight.Client/svg/logo.svg"
alt=""/>
</div>
</div>

View File

@@ -24,7 +24,7 @@
<span
class="inline-grid shrink-0 align-middle">
<img class="h-8 rounded-full"
src="/svg/logo.svg"
src="/_content/Moonlight.Client/svg/logo.svg"
alt=""/>
</span>
<span class="truncate">Moonlight v2.1</span>
@@ -85,7 +85,7 @@
<div class="flex min-w-0 items-center gap-3">
<span class="inline-grid shrink-0 align-middle">
<img class="h-8 rounded-full"
src="/img/pfp_placeholder.png"
src="/_content/Moonlight.Client/img/pfp_placeholder.png"
alt=""/>
</span>
<div class="min-w-0">
@@ -118,7 +118,7 @@
<div data-slot="avatar"
class="inline-grid shrink-0 align-middle">
<img
class="h-8 rounded-full" src="/placeholder.jpg" alt=""/>
class="h-8 rounded-full" src="/_content/Moonlight.Client/svg/logo.svg" alt=""/>
</div>
<div class="truncate">Moonlight v2.1</div>
</div>

View File

@@ -1,6 +1,4 @@
@page "/admin/api/create"
@using System.Text.Json
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys

View File

@@ -1,6 +1,4 @@
@page "/admin/api/{Id:int}"
@using System.Text.Json
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys

View File

@@ -1,7 +1,6 @@
@page "/admin/system/files"
@using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.Services
@using MoonCore.Helpers
@using Moonlight.Client.Implementations
@using MoonCore.Blazor.FlyonUi.Files.Manager

View File

@@ -1,6 +1,4 @@
@page "/admin/users/create"
@using System.Text.Json
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using MoonCore.Blazor.FlyonUi.Forms

View File

@@ -1,6 +1,4 @@
@page "/admin/users/{Id:int}"
@using System.Text.Json
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses.Admin.Users

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -9,7 +9,7 @@
<Title>Moonlight.Shared</Title>
<PackageTags>shared</PackageTags>
<PackageId>Moonlight.Shared</PackageId>
<Version>2.1.1</Version>
<Version>2.1.3</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the shared classes for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>

View File

@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moonlight.ApiServer.Runtime
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moonlight.Client.Runtime", "Moonlight.Client.Runtime\Moonlight.Client.Runtime.csproj", "{72F21AA4-4721-4B4C-B2FF-CFDCBB1BCB05}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Runtime", "Runtime", "{DCE3A43F-ACA8-41C6-BE27-3B3AA033B843}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -38,5 +40,7 @@ Global
{72F21AA4-4721-4B4C-B2FF-CFDCBB1BCB05}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{97FC686D-BC8A-4145-90C7-CA86B598441E} = {DCE3A43F-ACA8-41C6-BE27-3B3AA033B843}
{72F21AA4-4721-4B4C-B2FF-CFDCBB1BCB05} = {DCE3A43F-ACA8-41C6-BE27-3B3AA033B843}
EndGlobalSection
EndGlobal