Refactored startup. Removed unused usings. Improved nuget package building. Switched to yaml for configuration. Moved asset files. Set correct context type for oauth2 pages. Updated versions

This commit is contained in:
2025-07-14 21:06:54 +02:00
parent 2b62fc141d
commit 14993b9fe7
48 changed files with 763 additions and 1214 deletions

View File

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

View File

@@ -1,132 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Logging;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Plugins;
namespace Moonlight.ApiServer;
public class CleanStartup
{
// Logger
private ILogger Logger;
// Configuration
private AppConfiguration Configuration;
// WebApplication Stuff
private WebApplication WebApplication;
private WebApplicationBuilder WebApplicationBuilder;
public async Task AddMoonlight(
WebApplicationBuilder builder,
string[] args,
IPluginStartup[]? plugins = null
)
{
}
public async Task AddMoonlight(
WebApplication application,
string[] args,
IPluginStartup[]? plugins = null
)
{
}
#region Misc
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;
}
#endregion
#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
}

View File

@@ -1,24 +1,45 @@
using MoonCore.Helpers; using MoonCore.Helpers;
using YamlDotNet.Serialization;
namespace Moonlight.ApiServer.Configuration; 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"; 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(); public DatabaseConfig Database { get; set; } = new();
[YamlMember(Description = "Settings regarding authentication")]
public AuthenticationConfig Authentication { get; set; } = new(); public AuthenticationConfig Authentication { get; set; } = new();
[YamlMember(Description = "These options are only meant for development purposes")]
public DevelopmentConfig Development { get; set; } = new(); 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 KestrelConfig Kestrel { get; set; } = new();
public MetricsData Metrics { 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 class DatabaseConfig 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 string Host { get; set; } = "your-database-host.name";
public int Port { get; set; } = 5432; public int Port { get; set; } = 5432;
@@ -29,15 +50,19 @@ public class AppConfiguration
public string Database { get; set; } = "db_name"; 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); 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; 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 bool EnableLocalOAuth2 { get; set; } = true;
public OAuth2Data OAuth2 { get; set; } = new(); public OAuth2Data OAuth2 { get; set; } = new();
public class OAuth2Data public record OAuth2Data
{ {
public string Secret { get; set; } = Formatter.GenerateString(32); public string Secret { get; set; } = Formatter.GenerateString(32);
public string ClientId { get; set; } = Formatter.GenerateString(8); public string ClientId { get; set; } = Formatter.GenerateString(8);
@@ -46,24 +71,32 @@ public class AppConfiguration
public string? AccessEndpoint { get; set; } public string? AccessEndpoint { get; set; }
public string? AuthorizationRedirect { 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 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 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 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; public bool Enable { get; set; } = false;
[YamlMember(Description = "The interval in which metrics are created, specified in seconds")]
public int Interval { get; set; } = 15; 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 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 public class User
{ {

View File

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

View File

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

View File

@@ -14,8 +14,8 @@
} }
<link href="manifest.webmanifest" rel="manifest" /> <link href="manifest.webmanifest" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="/img/icon-512.png" /> <link rel="apple-touch-icon" sizes="512x512" href="/_content/Moonlight.Client/img/icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/img/icon-192.png" /> <link rel="apple-touch-icon" sizes="192x192" href="/_content/Moonlight.Client/img/icon-192.png" />
</head> </head>
<body> <body>

View File

@@ -56,11 +56,11 @@ public partial class OAuth2Controller : Controller
throw new HttpApiException("Invalid oauth2 request", 400); throw new HttpApiException("Invalid oauth2 request", 400);
} }
Response.StatusCode = 200; string html;
if (view == "register") 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("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri); parameters.Add("RedirectUri", redirectUri);
@@ -71,7 +71,7 @@ public partial class OAuth2Controller : Controller
} }
else else
{ {
var html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters => html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
{ {
parameters.Add("ClientId", clientId); parameters.Add("ClientId", clientId);
parameters.Add("RedirectUri", redirectUri); parameters.Add("RedirectUri", redirectUri);
@@ -80,6 +80,10 @@ public partial class OAuth2Controller : Controller
await Response.WriteAsync(html); await Response.WriteAsync(html);
} }
await Results
.Text(html, "text/html")
.ExecuteAsync(HttpContext);
} }
[AllowAnonymous] [AllowAnonymous]
@@ -117,7 +121,6 @@ public partial class OAuth2Controller : Controller
var code = await GenerateCode(user); var code = await GenerateCode(user);
Response.Redirect($"{redirectUri}?code={code}"); Response.Redirect($"{redirectUri}?code={code}");
return;
} }
else else
{ {
@@ -125,39 +128,38 @@ public partial class OAuth2Controller : Controller
var code = await GenerateCode(user); var code = await GenerateCode(user);
Response.Redirect($"{redirectUri}?code={code}"); Response.Redirect($"{redirectUri}?code={code}");
return;
} }
} }
catch (HttpApiException e) catch (HttpApiException e)
{ {
errorMessage = e.Title; errorMessage = e.Title;
}
Response.StatusCode = 200; string html;
if (view == "register") if (view == "register")
{
var html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
{ {
parameters.Add("ClientId", clientId); html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
parameters.Add("RedirectUri", redirectUri); {
parameters.Add("ResponseType", responseType); parameters.Add("ClientId", clientId);
parameters.Add("ErrorMessage", errorMessage!); parameters.Add("RedirectUri", redirectUri);
}); parameters.Add("ResponseType", responseType);
parameters.Add("ErrorMessage", errorMessage!);
await Response.WriteAsync(html); });
} }
else else
{
var html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
{ {
parameters.Add("ClientId", clientId); html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
parameters.Add("RedirectUri", redirectUri); {
parameters.Add("ResponseType", responseType); parameters.Add("ClientId", clientId);
parameters.Add("ErrorMessage", errorMessage!); 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

@@ -12,7 +12,6 @@ using Moonlight.ApiServer.Models;
using Moonlight.ApiServer.Plugins; using Moonlight.ApiServer.Plugins;
using Moonlight.ApiServer.Services; using Moonlight.ApiServer.Services;
using OpenTelemetry.Metrics; using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Moonlight.ApiServer.Implementations.Startup; namespace Moonlight.ApiServer.Implementations.Startup;
@@ -87,7 +86,7 @@ public class CoreStartup : IPluginStartup
#region Client / Frontend #region Client / Frontend
if (configuration.Client.Enable) if (configuration.Frontend.EnableHosting)
{ {
builder.Services.AddSingleton(new FrontendConfigurationOption() builder.Services.AddSingleton(new FrontendConfigurationOption()
{ {

View File

@@ -14,7 +14,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId> <PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.2</Version> <Version>2.1.3</Version>
<Authors>Moonlight Panel</Authors> <Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description> <Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl> <PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -38,16 +38,6 @@
<PackageReference Include="Ben.Demystifier" Version="0.4.1" /> <PackageReference Include="Ben.Demystifier" Version="0.4.1" />
</ItemGroup> </ItemGroup>
<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\**\*" /> <Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" /> <Content Remove="storage\**\*" />
<None Remove="storage\**\*" /> <None Remove="storage\**\*" />

View File

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

View File

@@ -83,7 +83,7 @@ public class FrontendService
public async Task<Stream> GenerateZip() // TODO: Rework to be able to extract everything successfully 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 // 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); throw new HttpApiException("The hosting of the wasm client has been disabled", 400);
// Load and check wasm path // Load and check wasm path

View File

@@ -1,552 +0,0 @@
using System.Text;
using System.Text.Json;
using Hangfire;
using Hangfire.EntityFrameworkCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
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 StartupX
{
private string[] Args;
// Logging
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

@@ -1,66 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Plugins;
namespace Moonlight.ApiServer.Startup;
public partial class CleanStartup
{
// Logger
private ILogger Logger;
// Configuration
private AppConfiguration Configuration;
// WebApplication Stuff
private WebApplication WebApplication;
private WebApplicationBuilder WebApplicationBuilder;
public async Task AddMoonlight(
WebApplicationBuilder builder,
string[] args,
IPluginStartup[]? plugins = null
)
{
}
public async Task AddMoonlight(
WebApplication application,
string[] args,
IPluginStartup[]? plugins = null
)
{
}
#region Misc
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;
}
#endregion
}

View File

@@ -1,6 +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; namespace Moonlight.ApiServer.Startup;
public partial class CleanStartup 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

@@ -1,19 +1,17 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MoonCore.Extended.Extensions; using MoonCore.Extended.Extensions;
using MoonCore.Extensions; using MoonCore.Extensions;
using MoonCore.Helpers; using MoonCore.Helpers;
using Moonlight.ApiServer.Plugins;
namespace Moonlight.ApiServer.Startup; namespace Moonlight.ApiServer.Startup;
public partial class CleanStartup public partial class Startup
{ {
private Task RegisterBase(IPluginStartup[] pluginStartups) private Task RegisterBase()
{ {
WebApplicationBuilder.Services.AutoAddServices<CleanStartup>(); WebApplicationBuilder.Services.AutoAddServices<Startup>();
WebApplicationBuilder.Services.AddHttpClient(); WebApplicationBuilder.Services.AddHttpClient();
WebApplicationBuilder.Services.AddApiExceptionHandler(); WebApplicationBuilder.Services.AddApiExceptionHandler();
@@ -25,7 +23,7 @@ public partial class CleanStartup
var mvcBuilder = WebApplicationBuilder.Services.AddControllers(); var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
// Add plugin assemblies as application parts // Add plugin assemblies as application parts
foreach (var pluginStartup in pluginStartups.Select(x => x.GetType().Assembly).Distinct()) foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct())
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly); mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
return Task.CompletedTask; return Task.CompletedTask;
@@ -36,15 +34,6 @@ public partial class CleanStartup
WebApplication.UseRouting(); WebApplication.UseRouting();
WebApplication.UseExceptionHandler(); WebApplication.UseExceptionHandler();
if (Configuration.Client.Enable)
{
if (WebApplication.Environment.IsDevelopment())
WebApplication.UseWebAssemblyDebugging();
WebApplication.UseBlazorFrameworkFiles();
WebApplication.UseStaticFiles();
}
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -52,7 +41,7 @@ public partial class CleanStartup
{ {
WebApplication.MapControllers(); WebApplication.MapControllers();
if (Configuration.Client.Enable) if (Configuration.Frontend.EnableHosting)
WebApplication.MapFallbackToController("Index", "Frontend"); WebApplication.MapFallbackToController("Index", "Frontend");
return Task.CompletedTask; return Task.CompletedTask;

View File

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

View File

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

View File

@@ -1,6 +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; namespace Moonlight.ApiServer.Startup;
public partial class CleanStartup 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

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

View File

@@ -1,6 +1,73 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
namespace Moonlight.ApiServer.Startup; namespace Moonlight.ApiServer.Startup;
public partial class CleanStartup 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

@@ -5,12 +5,12 @@ using Moonlight.ApiServer.Plugins;
namespace Moonlight.ApiServer.Startup; namespace Moonlight.ApiServer.Startup;
public partial class CleanStartup public partial class Startup
{ {
private IServiceProvider PluginLoadServiceProvider; private IServiceProvider PluginLoadServiceProvider;
private IPluginStartup[] PluginStartups; private IPluginStartup[] PluginStartups;
private Task InitializePlugins(IPluginStartup[] pluginStartups) private Task InitializePlugins()
{ {
// Create service provider for starting up // Create service provider for starting up
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
@@ -25,8 +25,6 @@ public partial class CleanStartup
PluginLoadServiceProvider = serviceCollection.BuildServiceProvider(); PluginLoadServiceProvider = serviceCollection.BuildServiceProvider();
PluginStartups = pluginStartups;
return Task.CompletedTask; return Task.CompletedTask;
} }

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> <ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\js\moonCore.js" /> <_ContentIncludedByDefault Remove="wwwroot\js\moonCore.js" />
<_ContentIncludedByDefault Remove="wwwroot\js\moonlight.js" /> <_ContentIncludedByDefault Remove="wwwroot\js\moonlight.js" />
<_ContentIncludedByDefault Remove="wwwroot\svg\logo.svg" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,9 +1,20 @@
using Moonlight.Client; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Moonlight.Client.Runtime; using Moonlight.Client.Runtime;
using Moonlight.Client.Startup;
var startup = new Startup();
var pluginLoader = new PluginLoader(); var pluginLoader = new PluginLoader();
pluginLoader.Initialize(); 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> <PropertyGroup>
<PackageTags>frontend</PackageTags> <PackageTags>frontend</PackageTags>
<PackageId>Moonlight.Client</PackageId> <PackageId>Moonlight.Client</PackageId>
<Version>2.1.2</Version> <Version>2.1.3</Version>
<Authors>Moonlight Panel</Authors> <Authors>Moonlight Panel</Authors>
<Description>A build of the client for moonlight development</Description> <Description>A build of the client for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl> <PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -27,16 +27,6 @@
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.7" /> <PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.7" />
</ItemGroup> </ItemGroup>
<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\**\*" /> <Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" /> <Content Remove="storage\**\*" />
<None 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"> class="inline-grid shrink-0 align-middle">
<img <img
class="h-8 rounded-full" class="h-8 rounded-full"
src="/svg/logo.svg" src="/_content/Moonlight.Client/svg/logo.svg"
alt=""/> alt=""/>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
@page "/admin/users/{Id:int}" @page "/admin/users/{Id:int}"
@using System.Text.Json
@using MoonCore.Helpers @using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users @using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses.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> <Title>Moonlight.Shared</Title>
<PackageTags>shared</PackageTags> <PackageTags>shared</PackageTags>
<PackageId>Moonlight.Shared</PackageId> <PackageId>Moonlight.Shared</PackageId>
<Version>2.1.1</Version> <Version>2.1.3</Version>
<Authors>Moonlight Panel</Authors> <Authors>Moonlight Panel</Authors>
<Description>A build of the shared classes for moonlight development</Description> <Description>A build of the shared classes for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl> <PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>

View File

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