Started implementing fronted configuration. Upgraded mooncore. Made database calls asnyc

This commit is contained in:
2025-01-06 22:36:21 +01:00
parent d477e803ab
commit 8372cfad1b
12 changed files with 231 additions and 186 deletions

View File

@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Misc;
namespace Moonlight.ApiServer.Http.Controllers;
[ApiController]
public class FrontendController : Controller
{
private readonly AppConfiguration Configuration;
private readonly PluginService PluginService;
private readonly AssetService AssetService;
public FrontendController(
AppConfiguration configuration,
PluginService pluginService,
AssetService assetService
)
{
Configuration = configuration;
PluginService = pluginService;
AssetService = assetService;
}
[HttpGet("frontend.json")]
public async Task<FrontendConfiguration> GetConfiguration()
{
var configuration = new FrontendConfiguration()
{
Title = "Moonlight",
ApiUrl = Configuration.PublicUrl,
HostEnvironment = "ApiServer"
};
configuration.Plugins.Entrypoints = PluginService.HostedPluginsManifest.Entrypoints;
configuration.Plugins.Assemblies = PluginService.HostedPluginsManifest.Assemblies;
configuration.Scripts = AssetService.GetJavascriptAssets();
return configuration;
}
[HttpGet("plugins/{assemblyName}")]
public async Task GetPluginAssembly(string assemblyName)
{
var assembliesMap = PluginService.ClientAssemblyMap;
if (assembliesMap.ContainsKey(assemblyName))
throw new HttpApiException("The requested assembly could not be found", 404);
var path = assembliesMap[assemblyName];
await Results.File(path).ExecuteAsync(HttpContext);
}
}

View File

@@ -1,66 +0,0 @@
<html lang="en">
<head>
<title>Login to your moonlight account</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
</head>
<body>
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="https://gamecp.masuowo.xyz/api/core/asset/Core/svg/logo.svg" alt="Your Company">
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Sign in to your account</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="#" method="POST">
<div>
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label>
<div class="mt-2">
<input id="email" name="email" type="email" autocomplete="email" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
<div class="text-sm">
<a href="#" class="font-semibold text-indigo-600 hover:text-indigo-500">Forgot password?</a>
</div>
</div>
<div class="mt-2">
<input id="password" name="password" type="password" autocomplete="current-password" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
</div>
</div>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
</div>
</form>
<p class="mt-10 text-center text-sm text-gray-500">
Not a member?
@{
var registerUrl = $"?response_type={ResponseType}&client_id={ClientId}&redirect_uri={RedirectUri}&action=register";
}
<a href="@registerUrl" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Register now</a>
</p>
</div>
</div>
</body>
</html>
@code
{
[Parameter]
public string ClientId { get; set; }
[Parameter]
public string RedirectUri { get; set; }
[Parameter]
public string ResponseType { get; set; }
}

View File

@@ -1,74 +0,0 @@
<html lang="en">
<head>
<title>Register your moonlight account</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
</head>
<body>
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="https://gamecp.masuowo.xyz/api/core/asset/Core/svg/logo.svg" alt="Your Company">
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Create your account</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="#" method="POST">
<div>
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Username</label>
<div class="mt-2">
<input id="username" name="username" type="text" autocomplete="username" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
</div>
</div>
<div>
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label>
<div class="mt-2">
<input id="email" name="email" type="email" autocomplete="email" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
<div class="text-sm">
<a href="#" class="font-semibold text-indigo-600 hover:text-indigo-500">Forgot password?</a>
</div>
</div>
<div class="mt-2">
<input id="password" name="password" type="password" autocomplete="current-password" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
</div>
</div>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign up</button>
</div>
</form>
<p class="mt-10 text-center text-sm text-gray-500">
Already a member?
@{
var loginUrl = $"?response_type={ResponseType}&client_id={ClientId}&redirect_uri={RedirectUri}&action=login";
}
<a href="@loginUrl" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Login now</a>
</p>
</div>
</div>
</body>
</html>
@code
{
[Parameter]
public string ClientId { get; set; }
[Parameter]
public string RedirectUri { get; set; }
[Parameter]
public string ResponseType { get; set; }
}

View File

@@ -1,3 +1,4 @@
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
@@ -15,17 +16,18 @@ public class LocalOAuth2Provider : ILocalProviderImplementation<User>
UserRepository = userRepository;
}
public Task SaveChanges(User model)
public async Task SaveChanges(User model)
{
UserRepository.Update(model);
return Task.CompletedTask;
await UserRepository.Update(model);
}
public Task<User?> LoadById(int id)
public async Task<User?> LoadById(int id)
{
var res = UserRepository.Get().FirstOrDefault(x => x.Id == id);
var res = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
return Task.FromResult(res);
return res;
}
public Task<User> Login(string email, string password)

View File

@@ -25,7 +25,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MoonCore" Version="1.8.1" />
<PackageReference Include="MoonCore.Extended" Version="1.2.5" />
<PackageReference Include="MoonCore.Extended" Version="1.2.6" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
@@ -39,6 +39,7 @@
<ItemGroup>
<Folder Include="Database\Migrations\" />
<Folder Include="Http\Controllers\Frontend\" />
</ItemGroup>
<ItemGroup>
@@ -47,4 +48,9 @@
</Content>
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Http\Controllers\OAuth2\Pages\Login.razor" />
<_ContentIncludedByDefault Remove="Http\Controllers\OAuth2\Pages\Register.razor" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,41 @@
using System.Runtime.Loader;
using MoonCore.Plugins;
using Moonlight.Shared.Misc;
namespace Moonlight.Client.Implementations;
public class RemotePluginSource : IPluginSource
{
private readonly FrontendConfiguration Configuration;
private readonly ILogger<RemotePluginSource> Logger;
private readonly HttpClient HttpClient;
public RemotePluginSource(
FrontendConfiguration configuration,
ILogger<RemotePluginSource> logger,
HttpClient httpClient
)
{
Configuration = configuration;
Logger = logger;
HttpClient = httpClient;
}
public async Task Load(AssemblyLoadContext loadContext, List<string> entrypoints)
{
entrypoints.AddRange(Configuration.Plugins.Entrypoints);
foreach (var assembly in Configuration.Plugins.Assemblies)
{
try
{
var fileStream = await HttpClient.GetStreamAsync($"plugins/{assembly}");
loadContext.LoadFromStream(fileStream);
}
catch (Exception e)
{
Logger.LogCritical("Unable to load plugin assembly '{assembly}': {e}", assembly, e);
}
}
}
}

View File

@@ -27,7 +27,7 @@
<PackageReference Include="MoonCore" Version="1.8.1" />
<PackageReference Include="MoonCore.Blazor" Version="1.2.8" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.2.3" />
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.2.4" />
</ItemGroup>
<!--

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
@@ -11,38 +12,40 @@ using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.PluginFramework.Extensions;
using MoonCore.Plugins;
using Moonlight.Client.Implementations;
using Moonlight.Client.Interfaces;
using Moonlight.Client.Services;
using Moonlight.Client.UI;
using Moonlight.Client.UI.Forms;
using Moonlight.Shared.Http.Responses.Assets;
using Moonlight.Shared.Http.Responses.PluginsStream;
using Moonlight.Shared.Misc;
namespace Moonlight.Client;
public class Startup
{
private string[] Args;
// Configuration
private FrontendConfiguration Configuration;
// Logging
private ILoggerProvider[] LoggerProviders;
private ILoggerFactory LoggerFactory;
private ILogger<Startup> Logger;
// WebAssemblyHost
private WebAssemblyHostBuilder WebAssemblyHostBuilder;
private WebAssemblyHost WebAssemblyHost;
// Plugin Loading
private PluginLoaderService PluginLoaderService;
private ApplicationAssemblyService ApplicationAssemblyService;
private IAppStartup[] PluginAppStartups;
public async Task Run(string[] args, Assembly[]? assemblies = null)
{
Args = args;
// Setup assembly storage
ApplicationAssemblyService = new()
{
@@ -53,10 +56,11 @@ public class Startup
await SetupLogging();
await CreateWebAssemblyHostBuilder();
await LoadConfiguration();
await LoadPlugins();
await InitializePlugins();
await RegisterLogging();
await RegisterBase();
await RegisterOAuth2();
@@ -89,10 +93,33 @@ public class Startup
}
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");
@@ -101,10 +128,10 @@ public class Startup
WebAssemblyHostBuilder.Services.AddScoped(_ =>
new HttpClient
{
BaseAddress = new Uri(WebAssemblyHostBuilder.HostEnvironment.BaseAddress)
BaseAddress = new Uri(Configuration.ApiUrl)
}
);
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
WebAssemblyHostBuilder.Services.AddScoped<DownloadService>();
WebAssemblyHostBuilder.Services.AddMoonCoreBlazorTailwind();
@@ -114,7 +141,7 @@ public class Startup
return Task.CompletedTask;
}
private Task RegisterOAuth2()
{
WebAssemblyHostBuilder.AddTokenAuthentication();
@@ -127,7 +154,7 @@ public class Startup
{
FormComponentRepository.Set<string, StringComponent>();
FormComponentRepository.Set<int, IntComponent>();
return Task.CompletedTask;
}
@@ -135,13 +162,10 @@ public class Startup
private async Task LoadAssets()
{
var apiClient = WebAssemblyHost.Services.GetRequiredService<HttpApiClient>();
var assetManifest = await apiClient.GetJson<FrontendAssetResponse>("api/assets");
var jsRuntime = WebAssemblyHost.Services.GetRequiredService<IJSRuntime>();
foreach (var javascriptFile in assetManifest.JavascriptFiles)
await jsRuntime.InvokeVoidAsync("moonlight.assets.loadJavascript", javascriptFile);
foreach (var scriptName in Configuration.Scripts)
await jsRuntime.InvokeVoidAsync("moonlight.assets.loadJavascript", scriptName);
}
#endregion
@@ -154,7 +178,7 @@ public class Startup
{
// We use moonlight itself as a plugin assembly
configuration.AddAssembly(typeof(Startup).Assembly);
configuration.AddAssemblies(ApplicationAssemblyService.AdditionalAssemblies);
configuration.AddAssemblies(ApplicationAssemblyService.PluginAssemblies);
@@ -162,7 +186,7 @@ public class Startup
configuration.AddInterface<IAppScreen>();
configuration.AddInterface<ISidebarItemProvider>();
});
return Task.CompletedTask;
}
@@ -177,23 +201,33 @@ public class Startup
LoggerFactory.CreateLogger<PluginLoaderService>()
);
// Build source from the retrieved data
var pluginsStreamUrl = $"{WebAssemblyHostBuilder.HostEnvironment.BaseAddress}api/assets/plugins";
PluginLoaderService.AddHttpHostedSource(pluginsStreamUrl);
// Create everything required to stream plugins
using var clientForStreaming = new HttpClient();
clientForStreaming.BaseAddress = new Uri(Configuration.HostEnvironment == "ApiServer"
? Configuration.ApiUrl
: WebAssemblyHostBuilder.HostEnvironment.BaseAddress
);
PluginLoaderService.AddSource(new RemotePluginSource(
Configuration,
LoggerFactory.CreateLogger<RemotePluginSource>(),
clientForStreaming
));
// Perform assembly loading
await PluginLoaderService.Load();
// Add plugin loader service to di for the Router/App.razor
ApplicationAssemblyService.PluginAssemblies = PluginLoaderService.PluginAssemblies;
WebAssemblyHostBuilder.Services.AddSingleton(ApplicationAssemblyService);
}
private Task InitializePlugins()
{
var initialisationServiceCollection = new ServiceCollection();
initialisationServiceCollection.AddLogging(builder => { builder.AddProviders(LoggerProviders); });
// Configure plugin loading by using the interface service
@@ -201,7 +235,7 @@ public class Startup
{
// We use moonlight itself as a plugin assembly
configuration.AddAssembly(typeof(Startup).Assembly);
configuration.AddAssemblies(ApplicationAssemblyService.AdditionalAssemblies);
configuration.AddAssemblies(ApplicationAssemblyService.PluginAssemblies);
@@ -260,7 +294,7 @@ public class Startup
#endregion
#region Logging
private Task SetupLogging()
{
LoggerProviders = LoggerBuildHelper.BuildFromConfiguration(configuration =>
@@ -274,10 +308,10 @@ public class Startup
LoggerFactory.AddProviders(LoggerProviders);
Logger = LoggerFactory.CreateLogger<Startup>();
return Task.CompletedTask;
}
private Task RegisterLogging()
{
WebAssemblyHostBuilder.Logging.ClearProviders();
@@ -285,7 +319,7 @@ public class Startup
return Task.CompletedTask;
}
#endregion
#region Web Application

View File

@@ -2,8 +2,7 @@
@using Moonlight.Client.Interfaces
@using Moonlight.Client.Services
@using Moonlight.Client.UI.Partials
@using MoonCore.Blazor.Components
@using Moonlight.Client.UI.Components
@using Moonlight.Shared.Misc
@inherits LayoutComponentBase
@@ -12,6 +11,9 @@
@inject ILogger<MainLayout> Logger
@inject IAppLoader[] AppLoaders
@inject IAppScreen[] AppScreens
@inject FrontendConfiguration Configuration
<PageTitle>@Configuration.Title</PageTitle>
@if (IsLoading)
{

View File

@@ -9,4 +9,6 @@
Welcome, @(IdentityService.Username)
</div>
<div class="text-gray-200 text-2xl">What do you want to do today?</div>
</div>
</div>
<div class="text-primary-500/10"></div>

View File

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

View File

@@ -0,0 +1,22 @@
namespace Moonlight.Shared.Misc;
public class FrontendConfiguration
{
public string Title { get; set; }
public string ApiUrl { get; set; }
public string HostEnvironment { get; set; }
public ThemeData Theme { get; set; } = new();
public string[] Scripts { get; set; }
public PluginData Plugins { get; set; } = new();
public class ThemeData
{
public Dictionary<string, Dictionary<int, string>> Variables { get; set; } = new();
}
public class PluginData
{
public string[] Assemblies { get; set; }
public string[] Entrypoints { get; set; }
}
}