Separating runtime from application code to improve building. Upgraded mooncore packages. Started switching to flyonui. Added PluginFramework plugin loading via mooncore

This commit is contained in:
2025-07-11 17:13:37 +02:00
parent 7e158d48c6
commit eaece9e334
67 changed files with 448 additions and 234 deletions

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
using Moonlight.ApiServer;
using Moonlight.ApiServer.Runtime;
var startup = new Startup();
var pluginLoader = new PluginLoader();
pluginLoader.Initialize();
await startup.Run(args, pluginLoader.Instances);

View File

@@ -0,0 +1,29 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5165",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"HTTP_PROXY": "",
"HTTPS_PROXY": ""
},
"hotReloadEnabled": true
},
"WASM Debug": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5165",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"HTTP_PROXY": "",
"HTTPS_PROXY": ""
},
"hotReloadEnabled": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
}
}
}

View File

@@ -16,14 +16,14 @@ namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
[Authorize(Policy = "permissions:admin.system.files")]
public class FilesController : Controller
{
private readonly string BaseDirectory = PathBuilder.Dir("storage");
private readonly string BaseDirectory = "storage";
private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes;
[HttpGet("list")]
public Task<FileSystemEntryResponse[]> List([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = PathBuilder.Dir(BaseDirectory, safePath);
var physicalPath = Path.Combine(BaseDirectory, safePath);
var entries = new List<FileSystemEntryResponse>();
@@ -84,7 +84,7 @@ public class FilesController : Controller
var positionToSkipTo = ChunkSize * chunkId;
var safePath = SanitizePath(path);
var physicalPath = PathBuilder.File(BaseDirectory, safePath);
var physicalPath = Path.Combine(BaseDirectory, safePath);
var baseDir = Path.GetDirectoryName(physicalPath);
if (!string.IsNullOrEmpty(baseDir))
@@ -113,11 +113,11 @@ public class FilesController : Controller
var oldSafePath = SanitizePath(oldPath);
var newSafePath = SanitizePath(newPath);
var oldPhysicalDirPath = PathBuilder.Dir(BaseDirectory, oldSafePath);
var oldPhysicalDirPath = Path.Combine(BaseDirectory, oldSafePath);
if (Directory.Exists(oldPhysicalDirPath))
{
var newPhysicalDirPath = PathBuilder.Dir(BaseDirectory, newSafePath);
var newPhysicalDirPath = Path.Combine(BaseDirectory, newSafePath);
Directory.Move(
oldPhysicalDirPath,
@@ -126,8 +126,8 @@ public class FilesController : Controller
}
else
{
var oldPhysicalFilePath = PathBuilder.File(BaseDirectory, oldSafePath);
var newPhysicalFilePath = PathBuilder.File(BaseDirectory, newSafePath);
var oldPhysicalFilePath = Path.Combine(BaseDirectory, oldSafePath);
var newPhysicalFilePath = Path.Combine(BaseDirectory, newSafePath);
System.IO.File.Move(
oldPhysicalFilePath,
@@ -142,13 +142,13 @@ public class FilesController : Controller
public Task Delete([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalDirPath = PathBuilder.Dir(BaseDirectory, safePath);
var physicalDirPath = Path.Combine(BaseDirectory, safePath);
if (Directory.Exists(physicalDirPath))
Directory.Delete(physicalDirPath, true);
else
{
var physicalFilePath = PathBuilder.File(BaseDirectory, safePath);
var physicalFilePath = Path.Combine(BaseDirectory, safePath);
System.IO.File.Delete(physicalFilePath);
}
@@ -160,7 +160,7 @@ public class FilesController : Controller
public Task CreateDirectory([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = PathBuilder.Dir(BaseDirectory, safePath);
var physicalPath = Path.Combine(BaseDirectory, safePath);
Directory.CreateDirectory(physicalPath);
return Task.CompletedTask;
@@ -170,7 +170,7 @@ public class FilesController : Controller
public async Task Download([FromQuery] string path)
{
var safePath = SanitizePath(path);
var physicalPath = PathBuilder.File(BaseDirectory, safePath);
var physicalPath = Path.Combine(BaseDirectory, safePath);
await using var fs = System.IO.File.Open(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await fs.CopyToAsync(Response.Body);
@@ -192,7 +192,7 @@ public class FilesController : Controller
private async Task CompressTarGz(string path, string[] itemsToCompress)
{
var safePath = SanitizePath(path);
var destination = PathBuilder.File(BaseDirectory, safePath);
var destination = Path.Combine(BaseDirectory, safePath);
await using var outStream = System.IO.File.Create(destination);
await using var gzoStream = new GZipOutputStream(outStream);
@@ -201,7 +201,7 @@ public class FilesController : Controller
foreach (var itemName in itemsToCompress)
{
var safeFilePath = SanitizePath(itemName);
var filePath = PathBuilder.File(BaseDirectory, safeFilePath);
var filePath = Path.Combine(BaseDirectory, safeFilePath);
var fi = new FileInfo(filePath);
@@ -210,7 +210,7 @@ public class FilesController : Controller
else
{
var safeDirePath = SanitizePath(itemName);
var dirPath = PathBuilder.Dir(BaseDirectory, safeDirePath);
var dirPath = Path.Combine(BaseDirectory, safeDirePath);
await AddDirectoryToTarGz(tarStream, dirPath);
}
@@ -267,7 +267,7 @@ public class FilesController : Controller
private async Task CompressZip(string path, string[] itemsToCompress)
{
var safePath = SanitizePath(path);
var destination = PathBuilder.File(BaseDirectory, safePath);
var destination = Path.Combine(BaseDirectory, safePath);
await using var outStream = System.IO.File.Create(destination);
await using var zipOutputStream = new ZipOutputStream(outStream);
@@ -275,7 +275,7 @@ public class FilesController : Controller
foreach (var itemName in itemsToCompress)
{
var safeFilePath = SanitizePath(itemName);
var filePath = PathBuilder.File(BaseDirectory, safeFilePath);
var filePath = Path.Combine(BaseDirectory, safeFilePath);
var fi = new FileInfo(filePath);
@@ -284,7 +284,7 @@ public class FilesController : Controller
else
{
var safeDirePath = SanitizePath(itemName);
var dirPath = PathBuilder.Dir(BaseDirectory, safeDirePath);
var dirPath = Path.Combine(BaseDirectory, safeDirePath);
await AddDirectoryToZip(zipOutputStream, dirPath);
}
@@ -350,7 +350,7 @@ public class FilesController : Controller
var safeDestination = SanitizePath(destination);
var safeArchivePath = SanitizePath(path);
var archivePath = PathBuilder.File(BaseDirectory, safeArchivePath);
var archivePath = Path.Combine(BaseDirectory, safeArchivePath);
await using var fs = System.IO.File.Open(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await using var gzipInputStream = new GZipInputStream(fs);
@@ -364,7 +364,7 @@ public class FilesController : Controller
break;
var safeFilePath = SanitizePath(entry.Name);
var fileDestination = PathBuilder.File(BaseDirectory, safeDestination, safeFilePath);
var fileDestination = Path.Combine(BaseDirectory, safeDestination, safeFilePath);
var parentFolder = Path.GetDirectoryName(fileDestination);
// Ensure parent directory exists, if it's not the base directory
@@ -393,7 +393,7 @@ public class FilesController : Controller
var safeDestination = SanitizePath(destination);
var safeArchivePath = SanitizePath(path);
var archivePath = PathBuilder.File(BaseDirectory, safeArchivePath);
var archivePath = Path.Combine(BaseDirectory, safeArchivePath);
await using var fs = System.IO.File.Open(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await using var zipInputStream = new ZipInputStream(fs);
@@ -409,7 +409,7 @@ public class FilesController : Controller
continue;
var safeFilePath = SanitizePath(entry.Name);
var fileDestination = PathBuilder.File(BaseDirectory, safeDestination, safeFilePath);
var fileDestination = Path.Combine(BaseDirectory, safeDestination, safeFilePath);
var parentFolder = Path.GetDirectoryName(fileDestination);
// Ensure parent directory exists, if it's not the base directory

View File

@@ -14,7 +14,7 @@ public class ThemeController : Controller
[Authorize(Policy = "permissions:admin.system.theme.update")]
public async Task Patch([FromBody] UpdateThemeRequest request)
{
var themePath = PathBuilder.File("storage", "theme.json");
var themePath = Path.Combine("storage", "theme.json");
await System.IO.File.WriteAllTextAsync(
themePath,

View File

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

View File

@@ -0,0 +1,51 @@
@using Moonlight.Shared.Misc
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@Configuration.Title</title>
<base href="/" />
@foreach (var style in Configuration.Styles)
{
<link rel="stylesheet" href="@style" />
}
<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" />
</head>
<body class="bg-gray-950 text-white font-inter h-full">
<div id="app">
<div class="flex h-screen justify-center items-center">
<div class="sm:max-w-lg">
<div id="blazor-loader-label" class="text-center mb-2 text-lg font-semibold"></div>
<div class="flex flex-col gap-1">
<div class="progress min-w-sm md:min-w-md" role="progressbar">
<div id="blazor-loader-progress" class="progress-bar"></div>
</div>
</div>
</div>
</div>
</div>
@foreach (var script in Configuration.Scripts)
{
<script src="@script"></script>
}
<script src="/_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>
</html>
@code
{
[Parameter] public FrontendConfiguration Configuration { get; set; }
}

View File

@@ -2,13 +2,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Helpers;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Models;
namespace Moonlight.ApiServer.Http.Controllers.Swagger;
[AllowAnonymous]
[Route("api/swagger")]
public class SwaggerController : Controller
{

View File

@@ -4,6 +4,7 @@ using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Implementations.Diagnose;
using Moonlight.ApiServer.Implementations.Metrics;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Models;
using Moonlight.ApiServer.Plugins;
using Moonlight.ApiServer.Services;
using OpenTelemetry.Metrics;
@@ -11,7 +12,6 @@ using OpenTelemetry.Trace;
namespace Moonlight.ApiServer.Implementations.Startup;
[PluginStartup]
public class CoreStartup : IPluginStartup
{
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
@@ -81,6 +81,23 @@ public class CoreStartup : IPluginStartup
#endregion
#region Client / Frontend
if (configuration.Client.Enable)
{
builder.Services.AddSingleton(new FrontendConfigurationOption()
{
Scripts =
[
"/_content/Moonlight.Client/js/moonlight.js", "/_content/Moonlight.Client/js/moonCore.js",
"/_content/Moonlight.Client/ace/ace.js"
],
Styles = ["/css/style.min.css"]
});
}
#endregion
return Task.CompletedTask;
}

View File

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

View File

@@ -6,7 +6,6 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj" />
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
</ItemGroup>
<ItemGroup>
@@ -30,13 +29,14 @@
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
<PackageReference Include="Hangfire.Core" Version="1.8.18" />
<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.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
<PackageReference Include="MoonCore" Version="1.8.8" />
<PackageReference Include="MoonCore.Extended" Version="1.3.4" />
<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.1" />
<PackageReference Include="MoonCore.Extended" Version="1.3.5" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.1" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />

View File

@@ -1,7 +0,0 @@
namespace Moonlight.ApiServer.Plugins;
[AttributeUsage(AttributeTargets.Class)]
public class PluginStartupAttribute : Attribute
{
}

View File

@@ -6,6 +6,7 @@ using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Http.Controllers.Frontend;
using Moonlight.ApiServer.Models;
using Moonlight.Shared.Misc;
@@ -17,23 +18,26 @@ public class FrontendService
private readonly AppConfiguration Configuration;
private readonly IWebHostEnvironment WebHostEnvironment;
private readonly IEnumerable<FrontendConfigurationOption> ConfigurationOptions;
private readonly IServiceProvider ServiceProvider;
public FrontendService(
AppConfiguration configuration,
IWebHostEnvironment webHostEnvironment,
IEnumerable<FrontendConfigurationOption> configurationOptions
IEnumerable<FrontendConfigurationOption> configurationOptions,
IServiceProvider serviceProvider
)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
ConfigurationOptions = configurationOptions;
ServiceProvider = serviceProvider;
}
public async Task<FrontendConfiguration> GetConfiguration()
{
var configuration = new FrontendConfiguration()
{
Title = "Moonlight",
Title = "Moonlight", // TODO: CONFIG
ApiUrl = Configuration.PublicUrl,
HostEnvironment = "ApiServer"
};
@@ -62,7 +66,20 @@ public class FrontendService
return configuration;
}
public async Task<Stream> GenerateZip()
public async Task<string> GenerateIndexHtml() // TODO: Cache
{
var configuration = await GetConfiguration();
return await ComponentHelper.RenderComponent<FrontendPage>(
ServiceProvider,
parameters =>
{
parameters["Configuration"] = configuration;
}
);
}
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)

View File

@@ -12,6 +12,7 @@ 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;
@@ -111,8 +112,7 @@ public class Startup
private Task CreateStorage()
{
Directory.CreateDirectory("storage");
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
Directory.CreateDirectory(Path.Combine("storage", "logs"));
return Task.CompletedTask;
}
@@ -142,7 +142,7 @@ public class Startup
private Task UseBase()
{
WebApplication.UseRouting();
WebApplication.UseApiExceptionHandler();
WebApplication.UseExceptionHandler();
if (Configuration.Client.Enable)
{
@@ -161,7 +161,7 @@ public class Startup
WebApplication.MapControllers();
if (Configuration.Client.Enable)
WebApplication.MapFallbackToFile("index.html");
WebApplication.MapFallbackToController("Index", "Frontend");
return Task.CompletedTask;
}
@@ -194,7 +194,7 @@ public class Startup
serviceCollection.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddProviders(LoggerProviders);
builder.AddAnsiConsole();
});
PluginLoadServiceProvider = serviceCollection.BuildServiceProvider();
@@ -202,8 +202,6 @@ public class Startup
// Collect startups
var pluginStartups = new List<IPluginStartup>();
pluginStartups.Add(new CoreStartup());
pluginStartups.AddRange(AdditionalPlugins); // Used by the development server
// Do NOT remove the following comment, as its used to place the plugin startup register calls
@@ -291,7 +289,7 @@ public class Startup
var configurationBuilder = new ConfigurationBuilder();
// Ensure configuration file exists
var jsonFilePath = PathBuilder.File(Directory.GetCurrentDirectory(), "storage", "app.json");
var jsonFilePath = Path.Combine(Directory.GetCurrentDirectory(), "storage", "app.json");
if (!File.Exists(jsonFilePath))
await File.WriteAllTextAsync(jsonFilePath, JsonSerializer.Serialize(new AppConfiguration()));
@@ -336,18 +334,8 @@ public class Startup
private Task SetupLogging()
{
LoggerProviders = LoggerBuildHelper.BuildFromConfiguration(configuration =>
{
configuration.Console.Enable = true;
configuration.Console.EnableAnsiMode = true;
configuration.FileLogging.Enable = true;
configuration.FileLogging.Path = PathBuilder.File("storage", "logs", "latest.log");
configuration.FileLogging.EnableLogRotation = true;
configuration.FileLogging.RotateLogNameTemplate = PathBuilder.File("storage", "logs", "apiserver.{0}.log");
});
LoggerFactory = new LoggerFactory();
LoggerFactory.AddProviders(LoggerProviders);
LoggerFactory.AddAnsiConsole();
Logger = LoggerFactory.CreateLogger<Startup>();
@@ -358,30 +346,33 @@ public class Startup
{
// Configure application logging
WebApplicationBuilder.Logging.ClearProviders();
WebApplicationBuilder.Logging.AddProviders(LoggerProviders);
WebApplicationBuilder.Logging.AddAnsiConsole();
WebApplicationBuilder.Logging.AddFile(Path.Combine("storage", "logs", "moonlight.log"));
// Logging levels
var logConfigPath = PathBuilder.File("storage", "logConfig.json");
var logConfigPath = Path.Combine("storage", "logConfig.json");
// Ensure logging config, add a default one is missing
if (!File.Exists(logConfigPath))
{
var logLevels = new Dictionary<string, string>
var defaultLogLevels = new Dictionary<string, string>
{
{ "Default", "Information" },
{ "Microsoft.AspNetCore", "Warning" },
{ "System.Net.Http.HttpClient", "Warning" }
};
var logLevelsJson = JsonSerializer.Serialize(logLevels);
var logConfig = "{\"LogLevel\":" + logLevelsJson + "}";
await File.WriteAllTextAsync(logConfigPath, logConfig);
var logLevelsJson = JsonSerializer.Serialize(defaultLogLevels);
await File.WriteAllTextAsync(logConfigPath, logLevelsJson);
}
// Add logging configuration
WebApplicationBuilder.Logging.AddConfiguration(
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
@@ -406,7 +397,6 @@ public class Startup
WebApplicationBuilder.Services.AddServiceCollectionAccessor();
WebApplicationBuilder.Services.AddScoped(typeof(DatabaseRepository<>));
WebApplicationBuilder.Services.AddScoped(typeof(CrudHelper<,>));
return Task.CompletedTask;
}
@@ -443,42 +433,8 @@ public class Startup
};
});
WebApplicationBuilder.Services.AddJwtInvalidation("coreAuthentication", options =>
{
options.InvalidateTimeProvider = async (provider, principal) =>
{
var userIdClaim = principal.Claims.FirstOrDefault(x => x.Type == "userId");
if (userIdClaim != null)
{
var userId = int.Parse(userIdClaim.Value);
var userRepository = provider.GetRequiredService<DatabaseRepository<User>>();
var user = await userRepository.Get().FirstOrDefaultAsync(x => x.Id == userId);
if (user == null)
return DateTime.MaxValue;
return user.TokenValidTimestamp;
}
var apiKeyIdClaim = principal.Claims.FirstOrDefault(x => x.Type == "apiKeyId");
if (apiKeyIdClaim != null)
{
var apiKeyId = int.Parse(apiKeyIdClaim.Value);
var apiKeyRepository = provider.GetRequiredService<DatabaseRepository<ApiKey>>();
var apiKey = await apiKeyRepository.Get().FirstOrDefaultAsync(x => x.Id == apiKeyId);
// If the api key exists, we don't want to invalidate the request.
// If it doesn't exist we want to invalidate the request
return apiKey == null ? DateTime.MaxValue : DateTime.MinValue;
}
return DateTime.MaxValue;
};
});
WebApplicationBuilder.Services.AddJwtBearerInvalidation("coreAuthentication");
WebApplicationBuilder.Services.AddScoped<IJwtInvalidateHandler, UserAuthInvalidation>();
WebApplicationBuilder.Services.AddAuthorization();
@@ -499,8 +455,6 @@ public class Startup
{
WebApplication.UseAuthentication();
WebApplication.UseJwtInvalidation();
WebApplication.UseAuthorization();
return Task.CompletedTask;

View File

@@ -0,0 +1,64 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.7"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<None Update="Styles\exports.css">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="Styles\package-lock.json">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="Styles\package.json">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Styles\preTailwind.css">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Styles\resolveNuget.js">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Styles\style.css">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\css\" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\js\moonCore.js" />
<_ContentIncludedByDefault Remove="wwwroot\js\moonlight.js" />
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,9 @@
using Moonlight.Client;
using Moonlight.Client.Runtime;
var startup = new Startup();
var pluginLoader = new PluginLoader();
pluginLoader.Initialize();
await startup.Run(args, pluginLoader.Instances);

View File

@@ -0,0 +1,14 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5165",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,6 +1,6 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback') layer;
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap') layer;
@import url("https://cdn.jsdelivr.net/npm/lucide-static/font/lucide.css") layer;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback') layer(base);
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap') layer(base);
@import url("https://cdn.jsdelivr.net/npm/lucide-static/font/lucide.css") layer(base);
@theme {
--font-inter: "Inter", var(--font-sans);

View File

@@ -6,9 +6,9 @@
"xml2js": "^0.6.2"
},
"scripts": {
"pretailwind-build": "node resolveNuget.js ../Moonlight.Client.csproj",
"pretailwind-build": "node resolveNuget.js ../Moonlight.Client.Runtime.csproj",
"tailwind-build": "npx tailwindcss -i style.css -o ../wwwroot/css/style.min.css",
"pretailwind": "node resolveNuget.js ../Moonlight.Client.csproj",
"pretailwind": "node resolveNuget.js ../Moonlight.Client.Runtime.csproj",
"tailwind": "npx tailwindcss -i style.css -o ../wwwroot/css/style.min.css --watch"
}
}

View File

@@ -13,4 +13,9 @@
@source "../**/*.razor";
@source "../**/*.cs";
@source "../**/*.html";
@source "../../Moonlight.Client/**/*.razor";
@source "../../Moonlight.Client/**/*.cs";
@source "../../Moonlight.Client/**/*.html";
@source "./mappings/*.map";

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
// Global using directives
global using Microsoft.AspNetCore.Components.Web;
global using Microsoft.JSInterop;
global using Microsoft.Extensions.Logging;
global using MoonCore.Blazor.FlyonUi.Components;

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.Client.Interfaces;
using Moonlight.Client.Plugins;

View File

@@ -1,7 +1,6 @@
using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Fm;
using MoonCore.Blazor.Tailwind.Fm.Models;
using MoonCore.Blazor.Tailwind.Services;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
using Moonlight.Shared.Http.Responses.Admin.Sys;

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
@@ -22,11 +22,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazor-ApexCharts" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all" />
<PackageReference Include="MoonCore" Version="1.8.8" />
<PackageReference Include="MoonCore.Blazor" Version="1.3.0" />
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.4.7" />
<PackageReference Include="MoonCore" Version="1.9.1" />
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.4" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
@@ -56,4 +54,10 @@
<ItemGroup>
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Styles\" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\css\style.min.css" />
</ItemGroup>
</Project>

View File

@@ -1,7 +0,0 @@
namespace Moonlight.Client.Plugins;
[AttributeUsage(AttributeTargets.Class)]
public class PluginStartupAttribute : Attribute
{
}

View File

@@ -1,13 +0,0 @@
using Moonlight.Client;
var startup = new Startup();
try
{
await startup.Run(args);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}

View File

@@ -2,8 +2,8 @@
using System.Web;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using MoonCore.Blazor.FlyonUi.Auth;
using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Auth;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Requests.Auth;

View File

@@ -1,5 +1,3 @@
using Microsoft.JSInterop;
namespace Moonlight.Client.Services;
public class WindowService

View File

@@ -1,21 +1,18 @@
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Blazor.FlyonUi;
using MoonCore.Blazor.FlyonUi.Auth;
using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Extensions;
using MoonCore.Blazor.Tailwind.Auth;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.Logging;
using MoonCore.Permissions;
using Moonlight.Client.Implementations;
using Moonlight.Client.Interfaces;
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;
@@ -27,7 +24,6 @@ public class Startup
private FrontendConfiguration Configuration;
// Logging
private ILoggerProvider[] LoggerProviders;
private ILoggerFactory LoggerFactory;
private ILogger<Startup> Logger;
@@ -143,12 +139,13 @@ public class Startup
});
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
WebAssemblyHostBuilder.Services.AddMoonCoreBlazorTailwind();
WebAssemblyHostBuilder.Services.AddFileManagerOperations();
WebAssemblyHostBuilder.Services.AddFlyonUiServices();
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
WebAssemblyHostBuilder.Services.AddScoped<ThemeService>();
WebAssemblyHostBuilder.Services.AutoAddServices<Program>();
WebAssemblyHostBuilder.Services.AutoAddServices<Startup>();
return Task.CompletedTask;
}
@@ -179,7 +176,7 @@ public class Startup
startupSc.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddProviders(LoggerProviders);
builder.AddAnsiConsole();
});
PluginLoadServiceProvider = startupSc.BuildServiceProvider();
@@ -187,8 +184,6 @@ public class Startup
// Collect startups
var pluginStartups = new List<IPluginStartup>();
pluginStartups.Add(new CoreStartup());
pluginStartups.AddRange(AdditionalPlugins); // Used by the development server
// Do NOT remove the following comment, as its used to place the plugin startup register calls
@@ -259,15 +254,8 @@ public class Startup
private Task SetupLogging()
{
LoggerProviders = LoggerBuildHelper.BuildFromConfiguration(configuration =>
{
configuration.Console.Enable = true;
configuration.Console.EnableAnsiMode = true;
configuration.FileLogging.Enable = false;
});
LoggerFactory = new LoggerFactory();
LoggerFactory.AddProviders(LoggerProviders);
LoggerFactory.AddAnsiConsole();
Logger = LoggerFactory.CreateLogger<Startup>();
@@ -277,7 +265,7 @@ public class Startup
private Task RegisterLogging()
{
WebAssemblyHostBuilder.Logging.ClearProviders();
WebAssemblyHostBuilder.Logging.AddProviders(LoggerProviders);
WebAssemblyHostBuilder.Logging.AddAnsiConsole();
return Task.CompletedTask;
}

View File

@@ -22,7 +22,7 @@
<ModalLauncher/>
<div id="blazor-error-ui" class="fixed bottom-0 left-0 w-full z-50">
<div class="bg-danger-600 text-white p-4 flex flex-row justify-between items-center">
<div class="bg-error text-white p-4 flex flex-row justify-between items-center">
<div class="flex items-center">
<i class="icon-bomb text-lg text-white me-2"></i>
<span>An unhandled error has occurred.</span>

View File

@@ -1,12 +1,11 @@
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using MoonCore.Blazor.Tailwind.Auth
@using MoonCore.Blazor.FlyonUi.Auth
@using Moonlight.Client.Interfaces
@using Moonlight.Client.Models
@using Moonlight.Client.UI.Layouts
@inject ToastService ToastService
@inject NavigationManager Navigation
@inject AuthenticationStateManager AuthStateManager
@inject IEnumerable<ISidebarItemProvider> SidebarItemProviders

View File

@@ -9,7 +9,8 @@
@inject FrontendConfiguration FrontendConfiguration
@inject ThemeService ThemeService
@inject ToastService ToastService
@inject DownloadService DownloadService
@* @inject DownloadService DownloadService *@
<div class="card card-body p-2">
<div class="flex flex-row items-center justify-end gap-x-2">
@@ -108,7 +109,7 @@
AddSetting("danger", "danger-300", 252, 165, 165);
AddSetting("danger", "danger-400", 248, 113, 113);
AddSetting("danger", "danger", 239, 68, 68);
AddSetting("danger", "danger-600", 220, 38, 38);
AddSetting("danger", "error", 220, 38, 38);
AddSetting("danger", "danger-700", 185, 28, 28);
AddSetting("danger", "danger-800", 153, 27, 27);
AddSetting("danger", "danger-900", 127, 29, 29);
@@ -192,7 +193,7 @@
{
if (FrontendConfiguration.HostEnvironment != "ApiServer")
{
await ToastService.Danger(
await ToastService.Error(
"Theme Settings",
"Unable to save the theme settings. If you are using a static host, you need to configure the colors in the frontend.json file"
);
@@ -215,7 +216,7 @@
var json = JsonSerializer.Serialize(ThemeService.Variables);
// Download the theme configuration
await DownloadService.DownloadString("theme.json", json);
//await DownloadService.DownloadString("theme.json", json);
await ToastService.Success("Successfully exported theme configuration");
}
@@ -224,7 +225,7 @@
{
if (!eventArgs.File.Name.EndsWith(".json"))
{
await ToastService.Danger("Only .json files are allowed");
await ToastService.Error("Only .json files are allowed");
return;
}

View File

@@ -10,7 +10,8 @@
@inject NavigationManager Navigation
@inject ToastService ToastService
@inject AlertService AlertService
@inject DownloadService DownloadService
@* @inject DownloadService DownloadService *@
<PageHeader Title="Create API Key">
<a href="/admin/api" class="btn btn-secondary">

View File

@@ -2,8 +2,8 @@
@using MoonCore.Helpers
@using MoonCore.Models
@using MoonCore.Blazor.Tailwind.Dt
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using MoonCore.Blazor.FlyonUi.DataTables
@inject HttpApiClient ApiClient
@inject AlertService AlertService

View File

@@ -3,7 +3,6 @@
@using System.Text.Json
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using MoonCore.Blazor.Tailwind.Input2
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation

View File

@@ -2,8 +2,8 @@
@using MoonCore.Helpers
@using MoonCore.Models
@using MoonCore.Blazor.Tailwind.Dt
@using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonCore.Blazor.FlyonUi.DataTables
@inject HttpApiClient ApiClient
@inject AlertService AlertService

View File

@@ -4,7 +4,6 @@
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonCore.Blazor.Tailwind.Input2
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation

View File

@@ -8,9 +8,7 @@
@using Microsoft.JSInterop
@using Moonlight.Client
@using MoonCore.Blazor.Tailwind.Components
@using MoonCore.Blazor.Tailwind.Alerts
@using MoonCore.Blazor.Tailwind.Helpers
@using MoonCore.Blazor.Tailwind.Modals
@using MoonCore.Blazor.Tailwind.Services
@using MoonCore.Blazor.Tailwind.Toasts
@using MoonCore.Blazor.FlyonUi.Components
@using MoonCore.Blazor.FlyonUi.Modals
@using MoonCore.Blazor.FlyonUi.Toasts
@using MoonCore.Blazor.FlyonUi.Alerts

View File

@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moonlight.Client</title>
<base href="/" />
<link rel="stylesheet" href="/css/style.min.css" />
<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" />
</head>
<body class="bg-gray-950 text-white font-inter h-full">
<div id="app">
<div class="flex h-screen justify-center items-center">
<div class="sm:max-w-lg">
<div id="blazor-loader-label" class="text-center mb-2 text-lg font-semibold"></div>
<div class="flex flex-col gap-1">
<div class="progress min-w-sm md:min-w-md" role="progressbar">
<div id="blazor-loader-progress" class="progress-bar"></div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/moonlight.js"></script>
<script src="/js/moonCore.js"></script>
<script src="/ace/ace.js"></script>
<script src="/_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>
</html>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -6,6 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moonlight.Client", "Moonlig
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moonlight.Shared", "Moonlight.Shared\Moonlight.Shared.csproj", "{C82E4F2A-91D2-4BC7-9AA7-241FDAAFC823}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moonlight.ApiServer.Runtime", "Moonlight.ApiServer.Runtime\Moonlight.ApiServer.Runtime.csproj", "{97FC686D-BC8A-4145-90C7-CA86B598441E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moonlight.Client.Runtime", "Moonlight.Client.Runtime\Moonlight.Client.Runtime.csproj", "{72F21AA4-4721-4B4C-B2FF-CFDCBB1BCB05}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -24,6 +28,14 @@ Global
{C82E4F2A-91D2-4BC7-9AA7-241FDAAFC823}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C82E4F2A-91D2-4BC7-9AA7-241FDAAFC823}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C82E4F2A-91D2-4BC7-9AA7-241FDAAFC823}.Release|Any CPU.Build.0 = Release|Any CPU
{97FC686D-BC8A-4145-90C7-CA86B598441E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{97FC686D-BC8A-4145-90C7-CA86B598441E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97FC686D-BC8A-4145-90C7-CA86B598441E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97FC686D-BC8A-4145-90C7-CA86B598441E}.Release|Any CPU.Build.0 = Release|Any CPU
{72F21AA4-4721-4B4C-B2FF-CFDCBB1BCB05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72F21AA4-4721-4B4C-B2FF-CFDCBB1BCB05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72F21AA4-4721-4B4C-B2FF-CFDCBB1BCB05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72F21AA4-4721-4B4C-B2FF-CFDCBB1BCB05}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection