Merge pull request #434 from Moonlight-Panel/v2_UpgradeMoonCore

Upgrade to new MoonCore version
This commit is contained in:
Masu Baumgartner
2024-07-06 11:41:34 +02:00
committed by GitHub
122 changed files with 2504 additions and 1588 deletions

View File

@@ -90,6 +90,16 @@ public class CoreConfiguration
[JsonProperty("DenyRegister")] [JsonProperty("DenyRegister")]
[Description("This disables the register function. No user will be able to sign up anymore. Its recommended to enable this for private instances")] [Description("This disables the register function. No user will be able to sign up anymore. Its recommended to enable this for private instances")]
public bool DenyRegister { get; set; } = false; public bool DenyRegister { get; set; } = false;
[JsonProperty("EnablePeriodicReAuth")]
[Description(
"If this option is enabled, every session will reauthenticate perdiodicly to track state changes in real time without the user refreshing the page")]
public bool EnablePeriodicReAuth { get; set; } = true;
[JsonProperty("PeriodicReAuthDelay")]
[Description(
"This option specifies how long the intervals are between reauthentications. The value is specified in minutes")]
public int PeriodicReAuthDelay { get; set; } = 5;
} }
public class SecurityData public class SecurityData

View File

@@ -3,14 +3,10 @@ using Microsoft.AspNetCore.Components;
using MoonCore.Abstractions; using MoonCore.Abstractions;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCore.Services; using MoonCore.Services;
using MoonCoreUI.Extensions;
using MoonCoreUI.Services;
using Moonlight.Core.Configuration; using Moonlight.Core.Configuration;
using Moonlight.Core.Database; using Moonlight.Core.Database;
using Moonlight.Core.Database.Entities; using Moonlight.Core.Database.Entities;
using Moonlight.Core.Implementations.Diagnose; using Moonlight.Core.Implementations.Diagnose;
using Moonlight.Core.Implementations.UI.Admin.AdminColumns;
using Moonlight.Core.Implementations.UI.Index;
using Moonlight.Core.Interfaces; using Moonlight.Core.Interfaces;
using Moonlight.Core.Interfaces.Ui.Admin; using Moonlight.Core.Interfaces.Ui.Admin;
using Moonlight.Core.Interfaces.UI.User; using Moonlight.Core.Interfaces.UI.User;
@@ -21,10 +17,16 @@ using Moonlight.Core.Models.Enums;
using Moonlight.Core.Repositories; using Moonlight.Core.Repositories;
using Moonlight.Core.Services; using Moonlight.Core.Services;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using MoonCore.Blazor.Extensions;
using MoonCore.Blazor.Services;
using MoonCore.Extensions;
using Moonlight.Core.Attributes; using Moonlight.Core.Attributes;
using Moonlight.Core.Http.Middleware; using Moonlight.Core.Http.Middleware;
using Moonlight.Core.Implementations.AdminDashboard;
using Moonlight.Core.Implementations.ApiDefinition; using Moonlight.Core.Implementations.ApiDefinition;
using Moonlight.Core.Implementations.UserDashboard;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationStateProvider = Moonlight.Core.Helpers.AuthenticationStateProvider;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
namespace Moonlight.Core; namespace Moonlight.Core;
@@ -55,26 +57,25 @@ public class CoreFeature : MoonlightFeature
builder.Services.AddDbContext<DataContext>(); builder.Services.AddDbContext<DataContext>();
// //
builder.Services.AddSingleton(new JwtService<CoreJwtType>(config.Security.Token)); builder.Services.AddSingleton(new JwtService<CoreJwtType>(
config.Security.Token,
context.LoggerFactory.CreateLogger<JwtService<CoreJwtType>>()
)
);
// Mooncore services // Mooncore services
builder.Services.AddScoped(typeof(Repository<>), typeof(GenericRepository<>)); builder.Services.AddScoped(typeof(Repository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<CookieService>();
builder.Services.AddScoped<FileDownloadService>();
builder.Services.AddScoped<AlertService>();
builder.Services.AddScoped<ToastService>();
builder.Services.AddScoped<ClipboardService>();
builder.Services.AddScoped<ModalService>();
builder.Services.AddMoonCoreUi(configuration => builder.Services.AddMoonCore(configuration =>
{ {
configuration.ToastJavascriptPrefix = "moonlight.toasts"; configuration.Identity.Token = config.Security.Token;
configuration.ModalJavascriptPrefix = "moonlight.modals"; configuration.Identity.PeriodicReAuthDelay = TimeSpan.FromMinutes(config.Authentication.PeriodicReAuthDelay);
configuration.AlertJavascriptPrefix = "moonlight.alerts"; configuration.Identity.EnablePeriodicReAuth = config.Authentication.EnablePeriodicReAuth;
configuration.ClipboardJavascriptPrefix = "moonlight.clipboard"; configuration.Identity.Provider = new AuthenticationStateProvider();
configuration.FileDownloadJavascriptPrefix = "moonlight.utils";
}); });
builder.Services.AddMoonCoreBlazor();
// Add external services and blazor/asp.net stuff // Add external services and blazor/asp.net stuff
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
@@ -226,9 +227,9 @@ public class CoreFeature : MoonlightFeature
{ {
using var scope = provider.CreateScope(); using var scope = provider.CreateScope();
var configService = scope.ServiceProvider.GetRequiredService<ConfigService<CoreConfiguration>>();
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>(); var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
var authenticationProvider = scope.ServiceProvider.GetRequiredService<IAuthenticationProvider>(); var authenticationProvider = scope.ServiceProvider.GetRequiredService<IAuthenticationProvider>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<CoreFeature>>();
if (!configService.Get().Authentication.UseDefaultAuthentication) if (!configService.Get().Authentication.UseDefaultAuthentication)
return; return;
@@ -246,7 +247,7 @@ public class CoreFeature : MoonlightFeature
if (registeredUser == null) if (registeredUser == null)
{ {
Logger.Warn("Unable to create default user. Register function returned null"); logger.LogWarning("Unable to create default user. Register function returned null");
return; return;
} }
@@ -255,7 +256,7 @@ public class CoreFeature : MoonlightFeature
user.Permissions = 9999; user.Permissions = 9999;
userRepo.Update(user); userRepo.Update(user);
Logger.Info($"Default login: Email: '{email}' Password: '{password}'"); logger.LogInformation("Default login: Email: '{email}' Password: '{password}'", email, password);
}); });
// Api // Api

View File

@@ -50,4 +50,9 @@ public class DataContext : DbContext
); );
} }
} }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
} }

View File

@@ -1,8 +1,15 @@
using MoonCore.Helpers; using MoonCore.Attributes;
using MoonCore.Helpers;
namespace Moonlight.Core.Events; namespace Moonlight.Core.Events;
[Singleton]
public class CoreEvents public class CoreEvents
{ {
public static SmartEventHandler OnMoonlightRestart { get; set; } = new(); public CoreEvents(ILogger<SmartEventHandler> logger)
{
OnMoonlightRestart = new(logger);
}
public SmartEventHandler OnMoonlightRestart { get; set; }
} }

View File

@@ -0,0 +1,54 @@
using MoonCore.Abstractions;
using MoonCore.Services;
using Moonlight.Core.Database.Entities;
namespace Moonlight.Core.Extensions;
public static class IdentityServiceExtensions
{
public static User GetUser(this IdentityService identityService)
{
return identityService.Storage.Get<User>();
}
public static Task<bool> HasFlag(this IdentityService identityService, string flag)
{
if (!identityService.IsAuthenticated)
return Task.FromResult(false);
var result = identityService.GetUser().Flags.Split(";").Contains(flag);
return Task.FromResult(result);
}
public static Task SetFlag(this IdentityService identityService, string flag, bool toggle)
{
if (!identityService.IsAuthenticated)
return Task.CompletedTask;
var user = identityService.GetUser();
// Rebuild flags
var flags = user.Flags.Split(";").ToList();
if (toggle)
{
if(!flags.Contains(flag))
flags.Add(flag);
}
else
{
if (flags.Contains(flag))
flags.Remove(flag);
}
user.Flags = string.Join(';', flags);
// Save changes
var serviceProvider = identityService.Storage.Get<IServiceProvider>();
var userRepo = serviceProvider.GetRequiredService<Repository<User>>();
userRepo.Update(user);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,50 @@
using MoonCore.Abstractions;
using MoonCore.Helpers;
using Moonlight.Core.Database.Entities;
namespace Moonlight.Core.Helpers;
public class AuthenticationStateProvider : MoonCore.Abstractions.AuthenticationStateProvider
{
public override Task<bool> IsValidIdentifier(IServiceProvider provider, string identifier)
{
if(!int.TryParse(identifier, out int searchId))
return Task.FromResult(false);
var userRepo = provider.GetRequiredService<Repository<User>>();
var result = userRepo.Get().Any(x => x.Id == searchId);
return Task.FromResult(result);
}
public override Task LoadFromIdentifier(IServiceProvider provider, string identifier, DynamicStorage storage)
{
if(!int.TryParse(identifier, out int searchId))
return Task.CompletedTask;
var userRepo = provider.GetRequiredService<Repository<User>>();
var user = userRepo.Get().FirstOrDefault(x => x.Id == searchId);
if(user == null)
return Task.CompletedTask;
storage.Set("User", user);
storage.Set("ServiceProvider", provider);
return Task.CompletedTask;
}
public override Task<DateTime> DetermineTokenValidTimestamp(IServiceProvider provider, string identifier)
{
if(!int.TryParse(identifier, out int searchId))
return Task.FromResult(DateTime.MaxValue);
var userRepo = provider.GetRequiredService<Repository<User>>();
var user = userRepo.Get().FirstOrDefault(x => x.Id == searchId);
if(user == null)
return Task.FromResult(DateTime.MaxValue);
return Task.FromResult(user.TokenValidTimestamp);
}
}

View File

@@ -1,12 +1,21 @@
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using MoonCore.Attributes;
using MoonCore.Helpers; using MoonCore.Helpers;
namespace Moonlight.Core.Helpers; namespace Moonlight.Core.Helpers;
public static class HostSystemHelper [Singleton]
public class HostSystemHelper
{ {
public static Task<string> GetOsName() private readonly ILogger<HostSystemHelper> Logger;
public HostSystemHelper(ILogger<HostSystemHelper> logger)
{
Logger = logger;
}
public Task<string> GetOsName()
{ {
try try
{ {
@@ -48,21 +57,20 @@ public static class HostSystemHelper
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn("Error retrieving os information"); Logger.LogWarning("Error retrieving os information: {e}", e);
Logger.Warn(e);
return Task.FromResult("N/A"); return Task.FromResult("N/A");
} }
} }
public static Task<long> GetMemoryUsage() public Task<long> GetMemoryUsage()
{ {
var process = Process.GetCurrentProcess(); var process = Process.GetCurrentProcess();
var bytes = process.PrivateMemorySize64; var bytes = process.PrivateMemorySize64;
return Task.FromResult(bytes); return Task.FromResult(bytes);
} }
public static Task<int> GetCpuUsage() public Task<int> GetCpuUsage()
{ {
var process = Process.GetCurrentProcess(); var process = Process.GetCurrentProcess();
var cpuTime = process.TotalProcessorTime; var cpuTime = process.TotalProcessorTime;

View File

@@ -9,13 +9,20 @@ namespace Moonlight.Core.Http.Controllers;
[Route("api/core/asset")] [Route("api/core/asset")]
public class AssetController : Controller public class AssetController : Controller
{ {
private readonly ILogger<AssetController> Logger;
public AssetController(ILogger<AssetController> logger)
{
Logger = logger;
}
[HttpGet("{name}/{*path}")] [HttpGet("{name}/{*path}")]
public async Task<ActionResult> Get(string name, string path) public async Task<ActionResult> Get(string name, string path)
{ {
// Check for path transversal attacks // Check for path transversal attacks
if (path.Contains("..") || name.Contains("..")) if (path.Contains("..") || name.Contains(".."))
{ {
Logger.Warn($"{HttpContext.Connection.RemoteIpAddress} tried to use path transversal attack: {name}/{path}"); Logger.LogWarning("{remoteIp} tried to use path transversal attack: {name}/{path}", HttpContext.Connection.RemoteIpAddress, name, path);
return NotFound(); return NotFound();
} }

View File

@@ -2,12 +2,11 @@
using System.Text; using System.Text;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonCore.Abstractions; using MoonCore.Abstractions;
using MoonCore.Helpers;
using MoonCore.Services; using MoonCore.Services;
using Moonlight.Core.Attributes; using Moonlight.Core.Attributes;
using Moonlight.Core.Configuration; using Moonlight.Core.Configuration;
using Moonlight.Core.Database.Entities; using Moonlight.Core.Database.Entities;
using Moonlight.Core.Services; using Moonlight.Core.Extensions;
namespace Moonlight.Core.Http.Controllers; namespace Moonlight.Core.Http.Controllers;
@@ -19,15 +18,17 @@ public class AvatarController : Controller
private readonly Repository<User> UserRepository; private readonly Repository<User> UserRepository;
private readonly ConfigService<CoreConfiguration> ConfigService; private readonly ConfigService<CoreConfiguration> ConfigService;
private readonly IdentityService IdentityService; private readonly IdentityService IdentityService;
private readonly ILogger<AvatarController> Logger;
public AvatarController( public AvatarController(
Repository<User> userRepository, Repository<User> userRepository,
IdentityService identityService, IdentityService identityService,
ConfigService<CoreConfiguration> configService) ConfigService<CoreConfiguration> configService, ILogger<AvatarController> logger)
{ {
UserRepository = userRepository; UserRepository = userRepository;
IdentityService = identityService; IdentityService = identityService;
ConfigService = configService; ConfigService = configService;
Logger = logger;
} }
[HttpGet] [HttpGet]
@@ -39,10 +40,10 @@ public class AvatarController : Controller
var token = Request.Cookies["token"]; var token = Request.Cookies["token"];
await IdentityService.Authenticate(token!); await IdentityService.Authenticate(token!);
if (!IdentityService.IsLoggedIn) if (!IdentityService.IsAuthenticated)
return StatusCode(403); return StatusCode(403);
return File(await GetAvatar(IdentityService.CurrentUser), "image/jpeg"); return File(await GetAvatar(IdentityService.GetUser()), "image/jpeg");
} }
[HttpGet("{id:int}")] [HttpGet("{id:int}")]
@@ -54,12 +55,12 @@ public class AvatarController : Controller
var token = Request.Cookies["token"]; var token = Request.Cookies["token"];
await IdentityService.Authenticate(token!); await IdentityService.Authenticate(token!);
if (!IdentityService.IsLoggedIn) if (!IdentityService.IsAuthenticated)
return StatusCode(403); return StatusCode(403);
if (ConfigService.Get().Security.EnforceAvatarPrivacy && // Do we need to enforce privacy? if (ConfigService.Get().Security.EnforceAvatarPrivacy && // Do we need to enforce privacy?
id != IdentityService.CurrentUser.Id && // is the user not viewing his own image? id != IdentityService.GetUser().Id && // is the user not viewing his own image?
IdentityService.CurrentUser.Permissions < 1000) // and not an admin? IdentityService.GetUser().Permissions < 1000) // and not an admin?
{ {
return StatusCode(403); return StatusCode(403);
} }
@@ -88,11 +89,10 @@ public class AvatarController : Controller
catch (Exception e) catch (Exception e)
{ {
if(e is HttpRequestException requestException && requestException.InnerException is IOException ioException) if(e is HttpRequestException requestException && requestException.InnerException is IOException ioException)
Logger.Warn($"Unable to fetch gravatar for user {user.Id}. Is moonlight inside a proxy requiring network?: {ioException.Message}"); Logger.LogWarning("Unable to fetch gravatar for user {userId}. Is moonlight inside a proxy requiring network?: {message}", user.Id, ioException.Message);
else else
{ {
Logger.Warn($"Unable to fetch gravatar for user {user.Id}"); Logger.LogWarning("Unable to fetch gravatar for user {userId}: {e}", user.Id, e);
Logger.Warn(e);
} }
return new MemoryStream(); return new MemoryStream();

View File

@@ -4,16 +4,18 @@ namespace Moonlight.Core.Http.Middleware;
public class DebugLogMiddleware public class DebugLogMiddleware
{ {
private readonly ILogger<DebugLogMiddleware> Logger;
private RequestDelegate Next; private RequestDelegate Next;
public DebugLogMiddleware(RequestDelegate next) public DebugLogMiddleware(RequestDelegate next, ILogger<DebugLogMiddleware> logger)
{ {
Next = next; Next = next;
Logger = logger;
} }
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context)
{ {
Logger.Debug($"[{context.Request.Method.ToUpper()}] {context.Request.Path}"); Logger.LogDebug("[{method}] {path}", context.Request.Method.ToUpper(), context.Request.Path);
await Next(context); await Next(context);
} }

View File

@@ -1,9 +1,9 @@
using MoonCoreUI.Helpers; using MoonCore.Blazor.Helpers;
using Moonlight.Core.Interfaces.Ui.Admin; using Moonlight.Core.Interfaces.Ui.Admin;
using Moonlight.Core.Models.Abstractions; using Moonlight.Core.Models.Abstractions;
using Moonlight.Core.UI.Components.Cards; using Moonlight.Core.UI.Components.Cards;
namespace Moonlight.Core.Implementations.UI.Admin.AdminColumns; namespace Moonlight.Core.Implementations.AdminDashboard;
public class UserCount : IAdminDashboardColumn public class UserCount : IAdminDashboardColumn
{ {

View File

@@ -1,9 +1,9 @@
using MoonCoreUI.Helpers; using MoonCore.Blazor.Helpers;
using Moonlight.Core.Interfaces.UI.User; using Moonlight.Core.Interfaces.UI.User;
using Moonlight.Core.Models.Abstractions; using Moonlight.Core.Models.Abstractions;
using Moonlight.Core.UI.Components.Cards; using Moonlight.Core.UI.Components.Cards;
namespace Moonlight.Core.Implementations.UI.Index; namespace Moonlight.Core.Implementations.UserDashboard;
public class GreetingMessages : IUserDashboardComponent public class GreetingMessages : IUserDashboardComponent
{ {

View File

@@ -9,6 +9,7 @@ public class PreInitContext
public List<Assembly> DiAssemblies { get; set; } = new(); public List<Assembly> DiAssemblies { get; set; } = new();
public Dictionary<string, List<string>> Assets { get; set; } = new(); public Dictionary<string, List<string>> Assets { get; set; } = new();
public PluginService Plugins { get; set; } public PluginService Plugins { get; set; }
public ILoggerFactory LoggerFactory { get; set; }
public void EnableDependencyInjection<T>() public void EnableDependencyInjection<T>()
{ {

View File

@@ -1,4 +1,4 @@
using MoonCoreUI.Components; using MoonCore.Blazor.Components;
namespace Moonlight.Core.Models.Abstractions.Feature; namespace Moonlight.Core.Models.Abstractions.Feature;

View File

@@ -1,9 +0,0 @@
using System.ComponentModel;
namespace Moonlight.Core.Models.Forms;
public class ChangeCookiesForm
{
[Description("This specifies if you would like to personalize your experience with optional cookies.")]
public bool UseOptionalCookies { get; set; } = false;
}

View File

@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using MoonCoreUI.Attributes;
namespace Moonlight.Core.Models.Forms; namespace Moonlight.Core.Models.Forms;
@@ -8,12 +7,12 @@ public class ChangePasswordForm
[Required(ErrorMessage = "You need to provide a password")] [Required(ErrorMessage = "You need to provide a password")]
[MinLength(8, ErrorMessage = "The password must be at least 8 characters long")] [MinLength(8, ErrorMessage = "The password must be at least 8 characters long")]
[MaxLength(256, ErrorMessage = "The password must not be longer than 256 characters")] [MaxLength(256, ErrorMessage = "The password must not be longer than 256 characters")]
[CustomFormType(Type = "password")] //TODO: [CustomFormType(Type = "password")]
public string Password { get; set; } public string Password { get; set; }
[Required(ErrorMessage = "You need to provide a password")] [Required(ErrorMessage = "You need to provide a password")]
[MinLength(8, ErrorMessage = "The password must be at least 8 characters long")] [MinLength(8, ErrorMessage = "The password must be at least 8 characters long")]
[MaxLength(256, ErrorMessage = "The password must not be longer than 256 characters")] [MaxLength(256, ErrorMessage = "The password must not be longer than 256 characters")]
[CustomFormType(Type = "password")] //TODO: [CustomFormType(Type = "password")]
public string RepeatedPassword { get; set; } public string RepeatedPassword { get; set; }
} }

View File

@@ -4,12 +4,6 @@ namespace Moonlight.Core.Models.Forms;
public class UpdateAccountForm public class UpdateAccountForm
{ {
[Required(ErrorMessage = "You need to provide an username")]
[MinLength(6, ErrorMessage = "The username is too short")]
[MaxLength(20, ErrorMessage = "The username cannot be longer than 20 characters")]
public string Username { get; set; } public string Username { get; set; }
[Required(ErrorMessage = "You need to provide an email address")]
[EmailAddress(ErrorMessage = "You need to enter a valid email address")]
public string Email { get; set; } = ""; public string Email { get; set; } = "";
} }

View File

@@ -1,6 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using MoonCoreUI.Attributes;
namespace Moonlight.Core.Models.Forms.Users; namespace Moonlight.Core.Models.Forms.Users;
@@ -17,7 +16,7 @@ public class UpdateUserForm
public string Email { get; set; } public string Email { get; set; }
[Description("This toggles the use of the two factor authentication")] [Description("This toggles the use of the two factor authentication")]
[RadioButtonBool("Enabled", "Disabled", TrueIcon = "bx-lock-alt", FalseIcon = "bx-lock-open-alt")] //TODO: [RadioButtonBool("Enabled", "Disabled", TrueIcon = "bx-lock-alt", FalseIcon = "bx-lock-open-alt")]
[DisplayName("Two factor authentication")] [DisplayName("Two factor authentication")]
public bool Totp { get; set; } = false; public bool Totp { get; set; } = false;
} }

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MoonCoreUI.Services; using MoonCore.Blazor.Services;
using Moonlight.Core.Services; using MoonCore.Services;
namespace Moonlight.Core.Models; namespace Moonlight.Core.Models;

View File

@@ -1,8 +1,7 @@
using System.Reflection; using System.Reflection;
using MoonCore.Blazor.Components;
using MoonCore.Extensions; using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.Services; using MoonCore.Services;
using MoonCoreUI.Components;
using Moonlight.Core.Configuration; using Moonlight.Core.Configuration;
using Moonlight.Core.Models.Abstractions.Feature; using Moonlight.Core.Models.Abstractions.Feature;
@@ -16,14 +15,17 @@ public class FeatureService
private readonly List<MoonlightFeature> Features = new(); private readonly List<MoonlightFeature> Features = new();
private readonly ConfigService<CoreConfiguration> ConfigService; private readonly ConfigService<CoreConfiguration> ConfigService;
public FeatureService(ConfigService<CoreConfiguration> configService) private readonly ILogger<FeatureService> Logger;
public FeatureService(ConfigService<CoreConfiguration> configService, ILogger<FeatureService> logger)
{ {
ConfigService = configService; ConfigService = configService;
Logger = logger;
} }
public Task Load() public Task Load()
{ {
Logger.Info("Loading features"); Logger.LogInformation("Loading features");
// TODO: Add dll loading here as well // TODO: Add dll loading here as well
@@ -43,24 +45,25 @@ public class FeatureService
if (feature == null) if (feature == null)
{ {
Logger.Warn($"Unable to construct {featureType.FullName} feature"); Logger.LogWarning("Unable to construct '{name}' feature", featureType.FullName);
continue; continue;
} }
Features.Add(feature); Features.Add(feature);
Logger.Info($"Loaded feature '{feature.Name}' by '{feature.Author}'"); Logger.LogInformation("Loaded feature '{name}' by '{author}'", feature.Name, feature.Author);
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task PreInit(WebApplicationBuilder builder, PluginService pluginService) public async Task PreInit(WebApplicationBuilder builder, PluginService pluginService, ILoggerFactory preRunLoggerFactory)
{ {
Logger.Info("Pre-initializing features"); Logger.LogInformation("Pre-initializing features");
PreInitContext.Builder = builder; PreInitContext.Builder = builder;
PreInitContext.Plugins = pluginService; PreInitContext.Plugins = pluginService;
PreInitContext.LoggerFactory = preRunLoggerFactory;
foreach (var feature in Features) foreach (var feature in Features)
{ {
@@ -70,8 +73,7 @@ public class FeatureService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error($"An error occured while performing pre init for feature '{feature.Name}'"); Logger.LogError("An error occured while performing pre init for feature '{name}': {e}", feature.Name, e);
Logger.Error(e);
} }
} }
@@ -82,7 +84,7 @@ public class FeatureService
public async Task Init(WebApplication application) public async Task Init(WebApplication application)
{ {
Logger.Info("Initializing features"); Logger.LogInformation("Initializing features");
var initContext = new InitContext() var initContext = new InitContext()
{ {
@@ -97,15 +99,14 @@ public class FeatureService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error($"An error occured while performing init for feature '{feature.Name}'"); Logger.LogError("An error occured while performing init for feature '{name}': {e}", feature.Name, e);
Logger.Error(e);
} }
} }
} }
public async Task UiInit() public async Task UiInit()
{ {
Logger.Info("Initializing feature uis"); Logger.LogInformation("Initializing feature uis");
foreach (var feature in Features) foreach (var feature in Features)
{ {
@@ -115,8 +116,7 @@ public class FeatureService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error($"An error occured while performing ui init for feature '{feature.Name}'"); Logger.LogError("An error occured while performing ui init for feature '{name}': {e}", feature.Name, e);
Logger.Error(e);
} }
} }
} }
@@ -138,8 +138,7 @@ public class FeatureService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error($"An error occured while performing session init for feature '{feature.Name}'"); Logger.LogError("An error occured while performing session init for feature '{name}': {e}", feature.Name, e);
Logger.Error(e);
} }
} }
} }
@@ -159,8 +158,7 @@ public class FeatureService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error($"An error occured while performing session dispose for feature '{feature.Name}'"); Logger.LogError("An error occured while performing session dispose for feature '{name}': {e}", feature.Name, e);
Logger.Error(e);
} }
} }
} }

View File

@@ -11,11 +11,13 @@ public class HotKeyService
private readonly IJSRuntime JsRuntime; private readonly IJSRuntime JsRuntime;
private readonly List<HotKeyModel> HotKeys = new(); private readonly List<HotKeyModel> HotKeys = new();
public SmartEventHandler<string> HotKeyPressed { get; set; } = new(); public SmartEventHandler<string> HotKeyPressed { get; set; }
public HotKeyService(IJSRuntime jsRuntime) public HotKeyService(IJSRuntime jsRuntime, ILogger<SmartEventHandler> eventHandlerLogger)
{ {
JsRuntime = jsRuntime; JsRuntime = jsRuntime;
HotKeyPressed = new(eventHandlerLogger);
} }
public async Task RegisterHotkey(string key, string modifier, string action) public async Task RegisterHotkey(string key, string modifier, string action)

View File

@@ -1,144 +0,0 @@
using MoonCore.Abstractions;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using MoonCore.Services;
using Moonlight.Core.Configuration;
using Moonlight.Core.Database.Entities;
using Moonlight.Core.Models.Enums;
namespace Moonlight.Core.Services;
[Scoped]
public class IdentityService : IDisposable
{
public User? CurrentUserNullable { get; private set; }
public User CurrentUser => CurrentUserNullable!;
public bool IsLoggedIn => CurrentUserNullable != null;
public SmartEventHandler OnAuthenticationStateChanged { get; set; } = new();
private string Token = "";
private readonly JwtService<CoreJwtType> JwtService;
private readonly ConfigService<CoreConfiguration> ConfigService;
private readonly Repository<User> UserRepository;
public IdentityService(
JwtService<CoreJwtType> jwtService,
ConfigService<CoreConfiguration> configService,
Repository<User> userRepository)
{
JwtService = jwtService;
ConfigService = configService;
UserRepository = userRepository;
}
public async Task<string> Authenticate(User user)
{
var duration = TimeSpan.FromDays(ConfigService.Get().Authentication.TokenDuration);
var token = await JwtService.Create(data =>
{
data.Add("UserId", user.Id.ToString());
data.Add("IssuedAt", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString());
}, CoreJwtType.Login, duration);
await Authenticate(token);
return token;
}
public async Task Authenticate(string token)
{
Token = token;
await Authenticate();
}
public async Task Authenticate(bool forceStateChange = false) // Can be used for authentication of the token as well
{
var lastUserId = CurrentUserNullable?.Id ?? -1;
await ProcessToken();
var currentUserId = CurrentUserNullable?.Id ?? -1;
if (lastUserId != currentUserId || forceStateChange)
await OnAuthenticationStateChanged.Invoke();
}
private async Task ProcessToken()
{
CurrentUserNullable = null;
// Check jwt signature
if (!await JwtService.Validate(Token, CoreJwtType.Login))
return;
var data = await JwtService.Decode(Token);
// Check for missing content
if(!data.ContainsKey("UserId") || !data.ContainsKey("IssuedAt"))
return;
// Load user
var userId = int.Parse(data["UserId"]);
var user = UserRepository
.Get()
.FirstOrDefault(x => x.Id == userId);
// Check if user was found
if(user == null)
return;
// Check token valid time
var issuedAt = long.Parse(data["IssuedAt"]);
var issuedAtDateTime = DateTimeOffset.FromUnixTimeSeconds(issuedAt).DateTime;
// If the valid time is newer then when the token was issued, the token is not longer valid
if (user.TokenValidTimestamp > issuedAtDateTime)
return;
CurrentUserNullable = user;
}
public Task<bool> HasFlag(string flag)
{
if (!IsLoggedIn)
return Task.FromResult(false);
var flags = CurrentUser.Flags.Split(";");
return Task.FromResult(flags.Contains(flag));
}
public Task SetFlag(string flag, bool toggle)
{
if (!IsLoggedIn)
throw new DisplayException("Unable to set flag while not logged in");
var flags = CurrentUser.Flags.Split(";").ToList();
if (toggle)
{
if(!flags.Contains(flag))
flags.Add(flag);
}
else
{
if (flags.Contains(flag))
flags.Remove(flag);
}
CurrentUser.Flags = string.Join(';', flags);
UserRepository.Update(CurrentUser);
return Task.CompletedTask;
}
public async void Dispose()
{
await OnAuthenticationStateChanged.ClearSubscribers();
}
}

View File

@@ -16,6 +16,14 @@ public class MoonlightService
public WebApplication Application { get; set; } // Do NOT modify using a plugin public WebApplication Application { get; set; } // Do NOT modify using a plugin
private readonly DateTime StartTimestamp = DateTime.UtcNow; private readonly DateTime StartTimestamp = DateTime.UtcNow;
private readonly CoreEvents CoreEvents;
private readonly ILogger<MoonlightService> Logger;
public MoonlightService(CoreEvents coreEvents, ILogger<MoonlightService> logger)
{
CoreEvents = coreEvents;
Logger = logger;
}
public MoonlightService() public MoonlightService()
{ {
@@ -50,7 +58,7 @@ public class MoonlightService
public async Task Restart() public async Task Restart()
{ {
Logger.Info("Restarting moonlight"); Logger.LogInformation("Restarting moonlight");
// Notify all users that this instance will restart // Notify all users that this instance will restart
await CoreEvents.OnMoonlightRestart.Invoke(); await CoreEvents.OnMoonlightRestart.Invoke();

View File

@@ -9,8 +9,12 @@ public class PluginService
private readonly Dictionary<Type, List<object>> ImplementationCache = new(); private readonly Dictionary<Type, List<object>> ImplementationCache = new();
private readonly List<MoonlightPlugin> Plugins = new(); private readonly List<MoonlightPlugin> Plugins = new();
public PluginService() private readonly ILogger<PluginService> Logger;
public PluginService(ILogger<PluginService> logger)
{ {
Logger = logger;
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins")); Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
} }
@@ -136,7 +140,7 @@ public class PluginService
if (pluginTypes.Length == 0) if (pluginTypes.Length == 0)
{ {
Logger.Info($"Loaded assembly as library. {dllFile}"); Logger.LogInformation("Loaded assembly as library: {dllFile}", dllFile);
continue; continue;
} }
@@ -146,19 +150,17 @@ public class PluginService
{ {
var plugin = await LoadFromType(pluginType); var plugin = await LoadFromType(pluginType);
Logger.Info($"Loaded plugin '{plugin.Name}'. Created by '{plugin.Author}'"); Logger.LogInformation("Loaded plugin '{name}'. Created by '{author}'", plugin.Name, plugin.Author);
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Fatal($"An error occured while loading plugin '{pluginType.FullName}'"); Logger.LogError("An error occured while loading plugin '{name}': {e}", pluginType.FullName, e);
Logger.Fatal(e);
} }
} }
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Fatal($"An error occured while loading assembly '{dllFile}'"); Logger.LogError("An error occured while loading assembly '{dllFile}': {e}", dllFile, e);
Logger.Fatal(e);
} }
} }
} }

View File

@@ -9,10 +9,12 @@ public class StartupJobService : BackgroundService
{ {
private readonly List<StartupJob> Jobs = new(); private readonly List<StartupJob> Jobs = new();
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
private readonly ILogger<StartupJobService> Logger;
public StartupJobService(IServiceProvider serviceProvider) public StartupJobService(IServiceProvider serviceProvider, ILogger<StartupJobService> logger)
{ {
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
Logger = logger;
} }
public Task AddJob(string name, TimeSpan delay, Func<IServiceProvider, Task> action) public Task AddJob(string name, TimeSpan delay, Func<IServiceProvider, Task> action)
@@ -29,7 +31,7 @@ public class StartupJobService : BackgroundService
public override Task Run() public override Task Run()
{ {
Logger.Info("Running startup jobs"); Logger.LogInformation("Running startup jobs");
foreach (var job in Jobs) foreach (var job in Jobs)
{ {
@@ -42,8 +44,7 @@ public class StartupJobService : BackgroundService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn($"The startup job '{job.Name}' failed:"); Logger.LogWarning("The startup job '{name}' failed: {e}", job.Name, e);
Logger.Warn(e);
} }
}); });
} }

View File

@@ -11,13 +11,17 @@ namespace Moonlight.Core.Services;
[Scoped] [Scoped]
public class UnloadService public class UnloadService
{ {
public SmartEventHandler OnUnloaded { get; set; } = new(); public SmartEventHandler OnUnloaded { get; set; }
private readonly IJSRuntime JsRuntime; private readonly IJSRuntime JsRuntime;
private readonly ILogger<UnloadService> Logger;
public UnloadService(IJSRuntime jsRuntime) public UnloadService(IJSRuntime jsRuntime, ILogger<SmartEventHandler> eventHandlerLogger, ILogger<UnloadService> logger)
{ {
JsRuntime = jsRuntime; JsRuntime = jsRuntime;
Logger = logger;
OnUnloaded = new(eventHandlerLogger);
} }
public async Task Initialize() public async Task Initialize()
@@ -28,8 +32,7 @@ public class UnloadService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error("An error occured while registering unload event handler"); Logger.LogError("An error occured while registering unload event handler: {e}", e);
Logger.Error(e);
} }
} }

View File

@@ -3,14 +3,14 @@
@using Moonlight.Core.Models.Abstractions @using Moonlight.Core.Models.Abstractions
@using Moonlight.Core.Models.Forms @using Moonlight.Core.Models.Forms
@using Moonlight.Core.Services
@using MoonCore.Exceptions @using MoonCore.Exceptions
@using MoonCoreUI.Services @using Moonlight.Core.Configuration
@inject IAuthenticationProvider AuthenticationProvider @inject IAuthenticationProvider AuthenticationProvider
@inject IdentityService IdentityService @inject IdentityService IdentityService
@inject CookieService CookieService @inject CookieService CookieService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject ConfigService<CoreConfiguration> ConfigService
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<SmartForm Model="Form" OnValidSubmit="OnValidSubmit"> <MCBForm Model="Form" OnValidSubmit="OnValidSubmit">
@if (RequiresTwoFactor) @if (RequiresTwoFactor)
{ {
<div class="fv-row mb-7"> <div class="fv-row mb-7">
@@ -58,7 +58,7 @@
@* OAuth2 Providers here *@ @* OAuth2 Providers here *@
</div> </div>
</div> </div>
</SmartForm> </MCBForm>
</div> </div>
</div> </div>
</div> </div>
@@ -90,10 +90,13 @@
throw new DisplayException("A user with these credential combination was not found"); throw new DisplayException("A user with these credential combination was not found");
// Generate token and authenticate // Generate token and authenticate
var token = await IdentityService.Authenticate(user); var token = await IdentityService.Login(
user.Id.ToString(),
TimeSpan.FromHours(ConfigService.Get().Authentication.TokenDuration)
);
// Save token for later use // Save token for later use
await CookieService.SetValue("token", token); await CookieService.SetValue("token", token, 30); //TODO: Add days to config option
// Forward the user if not on the specific page // Forward the user if not on the specific page
if(new Uri(Navigation.Uri).LocalPath.StartsWith("/login")) if(new Uri(Navigation.Uri).LocalPath.StartsWith("/login"))

View File

@@ -4,10 +4,7 @@
@using Moonlight.Core.Models.Abstractions @using Moonlight.Core.Models.Abstractions
@using Moonlight.Core.Models.Forms @using Moonlight.Core.Models.Forms
@using Moonlight.Core.Services
@using MoonCore.Exceptions @using MoonCore.Exceptions
@using MoonCore.Services
@using MoonCoreUI.Services
@using Moonlight.Core.Configuration @using Moonlight.Core.Configuration
@inject IAuthenticationProvider AuthenticationProvider @inject IAuthenticationProvider AuthenticationProvider
@@ -96,10 +93,13 @@
throw new DisplayException("Unable to create account"); throw new DisplayException("Unable to create account");
// Generate token and authenticate // Generate token and authenticate
var token = await IdentityService.Authenticate(user); var token = await IdentityService.Login(
user.Id.ToString(),
TimeSpan.FromHours(ConfigService.Get().Authentication.TokenDuration)
);
// Save token for later use // Save token for later use
await CookieService.SetValue("token", token); await CookieService.SetValue("token", token, 30); // TODO: config
// Forward the user if not on the specific page // Forward the user if not on the specific page
if(new Uri(Navigation.Uri).LocalPath.StartsWith("/register")) if(new Uri(Navigation.Uri).LocalPath.StartsWith("/register"))

View File

@@ -6,9 +6,10 @@
@using MoonCore.Exceptions @using MoonCore.Exceptions
@inject IdentityService IdentityService @inject IdentityService IdentityService
@inject AlertService AlertService
@inject IAuthenticationProvider AuthenticationProvider @inject IAuthenticationProvider AuthenticationProvider
@if (IdentityService.CurrentUser.Totp) @if (IdentityService.GetUser().Totp)
{ {
<div class="d-flex justify-content-center flex-column text-center"> <div class="d-flex justify-content-center flex-column text-center">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
@@ -42,7 +43,7 @@ else
var qrCodeData = qrGenerator.CreateQrCode var qrCodeData = qrGenerator.CreateQrCode
( (
$"otpauth://totp/{Uri.EscapeDataString(IdentityService.CurrentUser.Email)}?secret={Key}&issuer={Uri.EscapeDataString("Moonlight")}", $"otpauth://totp/{Uri.EscapeDataString(IdentityService.GetUser().Email)}?secret={Key}&issuer={Uri.EscapeDataString("Moonlight")}",
QRCodeGenerator.ECCLevel.Q QRCodeGenerator.ECCLevel.Q
); );
@@ -61,15 +62,15 @@ else
</div> </div>
</div> </div>
</div> </div>
<SmartForm Model="CodeForm" OnValidSubmit="VerifyAndEnable"> <div>
<input @bind="CodeForm.Code" type="number" class="form-control form-control-lg form-control-solid text-center my-8" placeholder="Enter authentication code" name="code"> <input @bind="CodeForm.Code" type="number" class="form-control form-control-lg form-control-solid text-center my-8" placeholder="Enter authentication code" name="code">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<button type="submit" class="btn btn-primary"> <WButton OnClick="VerifyAndEnable" CssClasses="btn btn-primary">
Continue Continue
<i class="bx bx-sm bx-right-arrow-alt"></i> <i class="bx bx-sm bx-right-arrow-alt"></i>
</button> </WButton>
</div>
</div> </div>
</SmartForm>
</div> </div>
} }
else else
@@ -139,10 +140,10 @@ else
private string Key = ""; private string Key = "";
private TwoFactorCodeForm CodeForm = new(); private TwoFactorCodeForm CodeForm = new();
private async Task Disable() private async Task Disable(ConfirmButton _)
{ {
await AuthenticationProvider.SetTwoFactorSecret(IdentityService.CurrentUser, ""); await AuthenticationProvider.SetTwoFactorSecret(IdentityService.GetUser(), "");
await IdentityService.Authenticate(true); await IdentityService.Authenticate();
HasStarted = false; HasStarted = false;
HasCompletedAppLinks = false; HasCompletedAppLinks = false;
@@ -170,11 +171,14 @@ else
var correctCode = totp.ComputeTotp(); var correctCode = totp.ComputeTotp();
if (CodeForm.Code != correctCode) if (CodeForm.Code != correctCode)
throw new DisplayException("Invalid code entered. Please try again"); {
await AlertService.Danger("Invalid code entered. Please try again");
return;
}
// Enable two factor auth for user // Enable two factor auth for user
await AuthenticationProvider.SetTwoFactorSecret(IdentityService.CurrentUser, Key); await AuthenticationProvider.SetTwoFactorSecret(IdentityService.GetUser(), Key);
await IdentityService.Authenticate(true); await IdentityService.Authenticate();
HasStarted = false; HasStarted = false;
HasCompletedAppLinks = false; HasCompletedAppLinks = false;

View File

@@ -1,6 +1,6 @@
@using MoonCore.Services @using MoonCore.Services
@using Moonlight.Core.Configuration @using Moonlight.Core.Configuration
@using Moonlight.Core.Services @using Moonlight.Core.Database.Entities
@inject IdentityService IdentityService @inject IdentityService IdentityService
@inject ConfigService<CoreConfiguration> ConfigService @inject ConfigService<CoreConfiguration> ConfigService
@@ -12,7 +12,7 @@
var greeting = GetGreetingMessage(); var greeting = GetGreetingMessage();
} }
@greeting.Item1 @greeting.Item1
<span class="text-info">@IdentityService.CurrentUser.Username</span> <span class="text-info">@IdentityService.GetUser().Username</span>
@greeting.Item2 @greeting.Item2
</span> </span>
</div> </div>

View File

@@ -1,7 +1,4 @@
@using Moonlight.Core.Services @inject IdentityService IdentityService
@using MoonCoreUI.Services
@inject IdentityService IdentityService
@inject CookieService CookieService @inject CookieService CookieService
<div class="app-header d-flex flex-column flex-stack"> <div class="app-header d-flex flex-column flex-stack">
@@ -25,13 +22,13 @@
<ConnectionIndicator/> <ConnectionIndicator/>
</div> </div>
@if (IdentityService.IsLoggedIn) @if (IdentityService.IsAuthenticated)
{ {
<div class="app-navbar-item ms-2 ms-lg-6 me-8"> <div class="app-navbar-item ms-2 ms-lg-6 me-8">
<div class="btn btn-active-light d-flex align-items-center bg-hover-light py-2 px-2 px-md-3" id="dropdownMenuLink" data-bs-toggle="dropdown"> <div class="btn btn-active-light d-flex align-items-center bg-hover-light py-2 px-2 px-md-3" id="dropdownMenuLink" data-bs-toggle="dropdown">
<div class="d-none d-md-flex flex-column align-items-end justify-content-center me-5"> <div class="d-none d-md-flex flex-column align-items-end justify-content-center me-5">
<span class="text-muted fs-7 fw-semibold lh-1 mb-2">Welcome,</span> <span class="text-muted fs-7 fw-semibold lh-1 mb-2">Welcome,</span>
<span class="text-gray-900 fs-base fw-bold lh-1">@@@IdentityService.CurrentUser.Username</span> <span class="text-gray-900 fs-base fw-bold lh-1">@@@IdentityService.GetUser().Username</span>
</div> </div>
<div class="symbol symbol-30px symbol-md-40px"> <div class="symbol symbol-30px symbol-md-40px">
@@ -47,10 +44,10 @@
</div> </div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div class="fw-bold d-flex align-items-center fs-5"> <div class="fw-bold d-flex align-items-center fs-5">
@IdentityService.CurrentUser.Username @IdentityService.GetUser().Username
</div> </div>
<span class="fw-semibold text-muted fs-7"> <span class="fw-semibold text-muted fs-7">
@IdentityService.CurrentUser.Email @IdentityService.GetUser().Email
</span> </span>
</div> </div>
</div> </div>
@@ -81,7 +78,7 @@
protected override Task OnAfterRenderAsync(bool firstRender) protected override Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
IdentityService.OnAuthenticationStateChanged += OnAuthenticationStateChanged; IdentityService.OnStateChanged += OnAuthenticationStateChanged;
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -94,9 +91,9 @@
private async Task Logout() private async Task Logout()
{ {
// Reset token // Reset token
await CookieService.SetValue("token", ""); await CookieService.SetValue("token", "", 30);
// Reset token in identity service // Reset token in identity service
await IdentityService.Authenticate(""); await IdentityService.Logout();
} }
} }

View File

@@ -56,13 +56,13 @@
} }
} }
@if (IdentityService.IsLoggedIn && IdentityService.CurrentUser.Permissions > 0) @if (IdentityService.IsAuthenticated && IdentityService.GetUser().Permissions > 0)
{ {
<div class="menu-item my-5"> <div class="menu-item my-5">
<div class="app-sidebar-separator separator"></div> <div class="app-sidebar-separator separator"></div>
</div> </div>
@foreach (var sidebarItem in FeatureService.UiContext.SidebarItems.Where(x => x.IsAdmin).OrderBy(x => x.Index).ToArray()) foreach (var sidebarItem in FeatureService.UiContext.SidebarItems.Where(x => x.IsAdmin).OrderBy(x => x.Index).ToArray())
{ {
if (IsMatch(sidebarItem)) if (IsMatch(sidebarItem))
{ {

View File

@@ -1,7 +1,4 @@
@using ApexCharts @using Moonlight.Core.Configuration
@using MoonCore.Services
@using Moonlight.Core.Configuration
@using Moonlight.Core.Services
@inject ConfigService<CoreConfiguration> ConfigService @inject ConfigService<CoreConfiguration> ConfigService
@inject IdentityService IdentityService @inject IdentityService IdentityService
@@ -12,17 +9,17 @@
<div style="pointer-events: all; max-width: var(--bs-breakpoint-sm)" class="w-100"> <div style="pointer-events: all; max-width: var(--bs-breakpoint-sm)" class="w-100">
<div class="card shadow-lg"> <div class="card shadow-lg">
<div class="card-body"> <div class="card-body">
<h3 class="mb-4">@ConfigService.Get().Customisation.CookieConsentBanner.BannerTitle</h3> <h3 class="mb-4">@BannerTitle</h3>
<p class="text-muted fs-6"> <p class="text-muted fs-6">
@ConfigService.Get().Customisation.CookieConsentBanner.BannerText @BannerText
</p> </p>
<span class="d-flex gap-5"> <span class="d-flex gap-5">
<a @onclick:preventDefault @onclick="Consent" class="btn btn-primary btn-sm cursor-pointer"> <WButton OnClick="() => SetAnswer(true)" CssClasses="btn btn-primary btn-smr">
<i class="bx bx-check"></i> @ConfigService.Get().Customisation.CookieConsentBanner.ConsentText <i class="bx bx-check"></i> @ConsentText
</a> </WButton>
<a @onclick:preventDefault @onclick="Decline" class="btn btn-secondary btn-sm cursor-pointer"> <WButton OnClick="() => SetAnswer(false)" CssClasses="btn btn-secondary btn-sm cursor-pointer">
<i class="bx bx-x"></i> @ConfigService.Get().Customisation.CookieConsentBanner.DeclineText <i class="bx bx-x"></i> @DeclineText
</a> </WButton>
</span> </span>
</div> </div>
</div> </div>
@@ -30,10 +27,25 @@
</div> </div>
} }
@code { @code
{
private bool ShowBanner; private bool ShowBanner;
private string BannerTitle;
private string BannerText;
private string ConsentText;
private string DeclineText;
protected override void OnInitialized()
{
var config = ConfigService.Get().Customisation.CookieConsentBanner;
BannerTitle = config.BannerTitle;
BannerText = config.BannerText;
ConsentText = config.ConsentText;
DeclineText = config.DeclineText;
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
@@ -47,25 +59,16 @@
} }
} }
private async Task Consent() private async Task SetAnswer(bool answer)
{ {
if (!IdentityService.IsLoggedIn) if (!IdentityService.IsAuthenticated)
return; return;
await IdentityService.SetFlag("CookieAsked", true); await IdentityService.SetFlag("CookieAsked", true);
if (answer)
await IdentityService.SetFlag("CookieConsent", true); await IdentityService.SetFlag("CookieConsent", true);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task Decline()
{
if (!IdentityService.IsLoggedIn)
return;
await IdentityService.SetFlag("CookieAsked", true);
await InvokeAsync(StateHasChanged);
}
} }

View File

@@ -52,7 +52,7 @@ else
if(permissionRequired == null) if(permissionRequired == null)
continue; continue;
if (IdentityService.CurrentUser.Permissions >= permissionRequired.Level) if (IdentityService.GetUser().Permissions >= permissionRequired.Level)
Allowed = true; Allowed = true;
} }

View File

@@ -4,12 +4,13 @@
@using Moonlight.Core.Services @using Moonlight.Core.Services
@inject IdentityService IdentityService @inject IdentityService IdentityService
@inject ILogger<SoftErrorHandler> Logger
@inherits ErrorBoundaryBase @inherits ErrorBoundaryBase
@if (Crashed || Exception != null) @if (Crashed || Exception != null)
{ {
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development" || (IdentityService.IsLoggedIn && IdentityService.CurrentUser.Permissions >= 9000)) if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development" || (IdentityService.IsAuthenticated && IdentityService.GetUser().Permissions >= 9000))
{ {
if (Exception != null) if (Exception != null)
{ {
@@ -74,9 +75,8 @@ else
Exception = exception; Exception = exception;
Crashed = true; Crashed = true;
var username = IdentityService.IsLoggedIn ? IdentityService.CurrentUser.Username : "Guest"; var username = IdentityService.IsAuthenticated ? IdentityService.GetUser().Username : "Guest";
Logger.Warn($"A crash occured in the view of '{username}'"); Logger.LogWarning("A crash occured in the view of '{username}': {exception}", username, exception);
Logger.Warn(exception);
} }
Recover(); Recover();

View File

@@ -1,6 +1,5 @@
@using Moonlight.Core.Services @using Moonlight.Core.Services
@using Moonlight.Core.UI.Components.Auth @using Moonlight.Core.UI.Components.Auth
@using MoonCore.Services
@using Moonlight.Core.Configuration @using Moonlight.Core.Configuration
@using Moonlight.Core.Events @using Moonlight.Core.Events
@@ -13,6 +12,7 @@
@inject UnloadService UnloadService @inject UnloadService UnloadService
@inject ConfigService<CoreConfiguration> ConfigService @inject ConfigService<CoreConfiguration> ConfigService
@inject ScopedStorageService ScopedStorageService @inject ScopedStorageService ScopedStorageService
@inject CoreEvents CoreEvents
@implements IDisposable @implements IDisposable
@@ -25,7 +25,7 @@
<AppHeader/> <AppHeader/>
<div class="app-wrapper flex-column flex-row-fluid"> <div class="app-wrapper flex-column flex-row-fluid">
@if (IdentityService.IsLoggedIn) @if (IdentityService.IsAuthenticated)
{ {
<AppSidebar/> <AppSidebar/>
} }
@@ -52,7 +52,7 @@
{ {
if (IsInitialized) if (IsInitialized)
{ {
if (IdentityService.IsLoggedIn) if (IdentityService.IsAuthenticated)
{ {
<PermissionChecker> <PermissionChecker>
@Body @Body
@@ -76,9 +76,9 @@
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="card card-body m-15 p-15"> <div class="card card-body m-15 p-15">
<LazyLoader Load="Load"> <LazyLoader Load="Load"
<span></span> UseDefaultValues="false"
</LazyLoader> TimeUntilSpinnerIsShown="TimeSpan.Zero" />
</div> </div>
</div> </div>
</div> </div>
@@ -99,6 +99,9 @@
</div> </div>
</div> </div>
<ModalLaunchPoint />
<ToastLaunchPoint />
@code @code
{ {
private bool IsInitialized = false; private bool IsInitialized = false;
@@ -110,7 +113,7 @@
// Base init // Base init
await lazyLoader.SetText("Initializing"); await lazyLoader.SetText("Initializing");
IdentityService.OnAuthenticationStateChanged += OnAuthenticationStateChanged; IdentityService.OnStateChanged += OnAuthenticationStateChanged;
CoreEvents.OnMoonlightRestart += async () => CoreEvents.OnMoonlightRestart += async () =>
{ {

View File

@@ -10,7 +10,7 @@
<span class="fs-2 fw-semibold"> <span class="fs-2 fw-semibold">
@* the @@@ looks weird, ik that, it will result in @username *@ @* the @@@ looks weird, ik that, it will result in @username *@
@@@IdentityService.CurrentUser.Username @@@IdentityService.GetUser().Username
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,10 @@
@page "/account" @page "/account"
@using System.ComponentModel.DataAnnotations
@using Moonlight.Core.Services @using Moonlight.Core.Services
@using Moonlight.Core.Models.Forms @using Moonlight.Core.Models.Forms
@using Mappy.Net @using Mappy.Net
@using MoonCoreUI.Services @using Moonlight.Core.Database.Entities
@using Moonlight.Core.Models.Abstractions @using Moonlight.Core.Models.Abstractions
@using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.UI.Components.Navigations
@@ -16,15 +17,13 @@
<span class="fs-2 fw-semibold"> <span class="fs-2 fw-semibold">
@* the @@@ looks weird, ik that, it will result in @username *@ @* the @@@ looks weird, ik that, it will result in @username *@
@@@IdentityService.CurrentUser.Username @@@IdentityService.GetUser().Username
</span> </span>
</div> </div>
</div> </div>
<AccountNavigation Index="0"/> <AccountNavigation Index="0"/>
<LazyLoader Load="Load" ShowAsCard="true">
<div class="row g-5"> <div class="row g-5">
<div class="col-md-3 col-12"> <div class="col-md-3 col-12">
<div class="card card-body h-100"> <div class="card card-body h-100">
@@ -43,36 +42,58 @@
</div> </div>
<div class="col-md-9 col-12"> <div class="col-md-9 col-12">
<div class="card"> <div class="card">
<SmartForm Model="Form" OnValidSubmit="Update">
<div class="card-body"> <div class="card-body">
<AutoForm TForm="UpdateAccountForm" Columns="12" Model="Form"/> <FastForm @ref="Form" Model="Model" OnConfigure="OnConfigure" />
</div> </div>
<div class="card-footer d-flex justify-content-end"> <div class="card-footer d-flex justify-content-end">
<button type="submit" class="btn btn-primary">Save changes</button> <WButton OnClick="SaveChanges" CssClasses="btn btn-primary">Save changes</WButton>
</div> </div>
</SmartForm>
</div> </div>
</div> </div>
</div> </div>
</LazyLoader>
@code @code
{ {
private UpdateAccountForm Form = new(); private UpdateAccountForm Model;
private FastForm<UpdateAccountForm> Form;
private Task Load(LazyLoader _) protected override void OnInitialized()
{ {
Form = Mapper.Map<UpdateAccountForm>(IdentityService.CurrentUser); // Create a copy of the user
Model = Mapper.Map<UpdateAccountForm>(IdentityService.GetUser());
return Task.CompletedTask;
} }
private async Task Update() private void OnConfigure(FastFormConfiguration<UpdateAccountForm> configuration)
{ {
await AuthenticationProvider.ChangeDetails(IdentityService.CurrentUser, Form.Email, Form.Username); configuration.AddProperty(x => x.Username)
.WithComponent<StringComponent>(component =>
{
component.ColumnsMd = 12;
})
.WithValidation(FastFormValidators.Required)
.WithValidation(RegexValidator.Create("^[a-z][a-z0-9]*$", "Usernames can only contain lowercase characters and numbers and should not start with a number"))
.WithValidation(x => x.Length >= 6 ? ValidationResult.Success : new("The username is too short"))
.WithValidation(x => x.Length <= 20 ? ValidationResult.Success : new("The username cannot be longer than 20 characters"));
configuration.AddProperty(x => x.Email)
.WithComponent<StringComponent>(component =>
{
component.ColumnsMd = 12;
component.Type = "email";
})
.WithValidation(FastFormValidators.Required)
.WithValidation(RegexValidator.Create("^.+@.+$", "You need to provide a valid email address"));
}
private async Task SaveChanges()
{
if(!await Form.Submit())
return;
await AuthenticationProvider.ChangeDetails(IdentityService.GetUser(), Model.Email, Model.Username);
await ToastService.Success("Successfully updated details"); await ToastService.Success("Successfully updated details");
// This will trigger a re-render as well as an update of the model // This will trigger a re-render as well as an update of the model
await IdentityService.Authenticate(true); await IdentityService.Authenticate();
} }
} }

View File

@@ -1,9 +1,9 @@
@page "/account/security" @page "/account/security"
@using System.ComponentModel.DataAnnotations
@using Moonlight.Core.Services @using Moonlight.Core.Services
@using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.UI.Components.Navigations
@using Moonlight.Core.Models.Forms @using Moonlight.Core.Models.Forms
@using MoonCoreUI.Services
@using Moonlight.Core.Models.Abstractions @using Moonlight.Core.Models.Abstractions
@using MoonCore.Exceptions @using MoonCore.Exceptions
@using Moonlight.Core.UI.Components.Auth @using Moonlight.Core.UI.Components.Auth
@@ -17,27 +17,22 @@
<span class="fs-2 fw-semibold"> <span class="fs-2 fw-semibold">
@* the @@@ looks weird, ik that, it will result in @username *@ @* the @@@ looks weird, ik that, it will result in @username *@
@@@IdentityService.CurrentUser.Username @@@IdentityService.GetUser().Username
</span> </span>
</div> </div>
</div> </div>
<AccountNavigation Index="1"/> <AccountNavigation Index="1"/>
<LazyLoader Load="Load">
<div class="row g-5"> <div class="row g-5">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<div class="card"> <div class="card">
<SmartForm Model="PasswordForm" OnValidSubmit="OnValidSubmitPassword">
<div class="card-body"> <div class="card-body">
<div class="row"> <FastForm @ref="PasswordForm" Model="PasswordModel" OnConfigure="OnConfigurePasswordForm" />
<AutoForm Model="PasswordForm" Columns="12" />
</div>
</div> </div>
<div class="card-footer d-flex justify-content-end"> <div class="card-footer d-flex justify-content-end">
<button class="btn btn-primary" type="submit">Save changes</button> <WButton OnClick="ChangePassword" CssClasses="btn btn-primary">Save changes</WButton>
</div> </div>
</SmartForm>
</div> </div>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
@@ -49,49 +44,77 @@
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<div class="card h-100"> <div class="card h-100">
<SmartForm Model="CookiesForm" OnValidSubmit="OnValidSubmitCookie">
<div class="card-body"> <div class="card-body">
<h3>Cookies</h3> <h3>Cookies</h3>
<div> <p class="my-2 text-muted fs-5">
This specifies if you would like to personalize your experience with optional cookies.
<AutoForm Model="CookiesForm"/> </p>
</div> <div class="form-check form-switch">
</div> @if (CookieConsent)
<div class="card-footer d-flex justify-content-end"> {
<button class="btn btn-primary" type="submit">Save changes</button> <input @onclick="() => SetCookieConsent(false)" class="form-check-input" type="checkbox" role="switch" checked="checked">
</div> }
</SmartForm> else
{
<input @onclick="() => SetCookieConsent(true)" class="form-check-input" type="checkbox" role="switch">
}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</LazyLoader>
@code @code
{ {
private readonly ChangePasswordForm PasswordForm = new(); private ChangePasswordForm PasswordModel = new();
private FastForm<ChangePasswordForm> PasswordForm;
private ChangeCookiesForm CookiesForm = new(); private bool CookieConsent;
private async Task Load(LazyLoader lazyLoader) protected override async Task OnInitializedAsync()
{ {
CookiesForm.UseOptionalCookies = await IdentityService.HasFlag("CookieConsent"); CookieConsent = await IdentityService.HasFlag("CookieConsent");
} }
private async Task OnValidSubmitPassword() private async Task SetCookieConsent(bool flag)
{ {
if (PasswordForm.Password != PasswordForm.RepeatedPassword) await IdentityService.SetFlag("CookieConsent", flag);
throw new DisplayException("The passwords do not match");
await AuthenticationProvider.ChangePassword(IdentityService.CurrentUser, PasswordForm.Password);
await ToastService.Success("Successfully changed password");
await IdentityService.Authenticate(true);
}
private async Task OnValidSubmitCookie()
{
await IdentityService.SetFlag("CookieConsent", CookiesForm.UseOptionalCookies);
await ToastService.Success("Successfully changed cookie preferences");
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private void OnConfigurePasswordForm(FastFormConfiguration<ChangePasswordForm> configuration)
{
configuration.AddProperty(x => x.Password)
.WithComponent<StringComponent>(component =>
{
component.ColumnsMd = 6;
component.Type = "password";
})
.WithValidation(FastFormValidators.Required)
.WithValidation(x => x.Length >= 8 ? ValidationResult.Success : new("The password must be at least 8 characters long"))
.WithValidation(x => x.Length <= 256 ? ValidationResult.Success : new("The password must not be longer than 256 characters"));
configuration.AddProperty(x => x.RepeatedPassword)
.WithComponent<StringComponent>(component =>
{
component.ColumnsMd = 6;
component.Type = "password";
});
}
private async Task ChangePassword()
{
if(!await PasswordForm.Submit())
return;
if (PasswordModel.Password != PasswordModel.RepeatedPassword)
throw new DisplayException("The passwords do not match");
await AuthenticationProvider.ChangePassword(IdentityService.GetUser(), PasswordModel.Password);
await ToastService.Success("Successfully changed password");
await IdentityService.Authenticate();
}
} }

View File

@@ -21,11 +21,11 @@
<div class="mt-5"> <div class="mt-5">
<div class="card card-body py-3 px-5"> <div class="card card-body py-3 px-5">
<LazyLoader Load="LoadApis"> <LazyLoader Load="LoadApis">
<CrudTable TItem="ApiModel" ItemSource="Apis" PageSize="100" ShowPagination="false"> <MCBTable TItem="ApiModel" ItemSource="Apis" PageSize="100" ShowPagination="false">
<CrudColumn TItem="ApiModel" Field="@(x => x.Id)" Title="Id" /> <MCBColumn TItem="ApiModel" Field="@(x => x.Id)" Title="Id" />
<CrudColumn TItem="ApiModel" Field="@(x => x.Name)" Title="Name" /> <MCBColumn TItem="ApiModel" Field="@(x => x.Name)" Title="Name" />
<CrudColumn TItem="ApiModel" Field="@(x => x.Version)" Title="Version" /> <MCBColumn TItem="ApiModel" Field="@(x => x.Version)" Title="Version" />
<CrudColumn TItem="ApiModel"> <MCBColumn TItem="ApiModel">
<Template> <Template>
<div class="text-end"> <div class="text-end">
@if (ConfigService.Get().Development.EnableApiReference) @if (ConfigService.Get().Development.EnableApiReference)
@@ -44,8 +44,8 @@
} }
</div> </div>
</Template> </Template>
</CrudColumn> </MCBColumn>
</CrudTable> </MCBTable>
</LazyLoader> </LazyLoader>
</div> </div>
</div> </div>

View File

@@ -1,10 +1,8 @@
@page "/admin/api/keys" @page "/admin/api/keys"
@using MoonCore.Abstractions @using MoonCore.Blazor.Models.FastForms
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonCoreUI.Services
@using Moonlight.Core.Database.Entities @using Moonlight.Core.Database.Entities
@using Moonlight.Core.Models.Forms.ApiKeys
@using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.UI.Components.Navigations
@inject ClipboardService ClipboardService @inject ClipboardService ClipboardService
@@ -15,17 +13,15 @@
<AdminApiNavigation Index="1"/> <AdminApiNavigation Index="1"/>
<div class="mt-5"> <div class="mt-5">
<AutoCrud TItem="ApiKey" <FastCrud TItem="ApiKey"
TCreateForm="CreateApiKeyForm" OnConfigure="OnConfigure"
TUpdateForm="UpdateApiKeyForm" OnConfigureCreate="OnConfigureFrom"
Loader="ApiKeysLoader" OnConfigureEdit="OnConfigureFrom">
ValidateAdd="ValidateAdd">
<View> <View>
<CrudColumn TItem="ApiKey" Field="@(x => x.Key)" Title="Key"> <MCBColumn TItem="ApiKey" Field="@(x => x.Key)" Title="Key">
<Template> <Template>
@{ @{
var apiKeyHalf = Formatter.CutInHalf(context!.Key); var apiKeyHalf = Formatter.CutInHalf(context!.Key);
var bogusHalf = Formatter.IntToStringWithLeadingZeros(69, apiKeyHalf.Length);
} }
<div> <div>
@@ -35,36 +31,54 @@
</span> </span>
</div> </div>
</Template> </Template>
</CrudColumn> </MCBColumn>
<CrudColumn TItem="ApiKey" Field="@(x => x.Description)" Title="Description"/> <MCBColumn TItem="ApiKey" Field="@(x => x.Description)" Title="Description"/>
<CrudColumn TItem="ApiKey" Field="@(x => x.CreatedAt)" Title="Created at"> <MCBColumn TItem="ApiKey" Field="@(x => x.CreatedAt)" Title="Created at">
<Template> <Template>
@Formatter.FormatDate(context!.CreatedAt) @Formatter.FormatDate(context!.CreatedAt)
</Template> </Template>
</CrudColumn> </MCBColumn>
<CrudColumn TItem="ApiKey" Field="@(x => x.ExpiresAt)" Title="Expires at"> <MCBColumn TItem="ApiKey" Field="@(x => x.ExpiresAt)" Title="Expires at">
<Template> <Template>
@Formatter.FormatDate(context!.ExpiresAt) @Formatter.FormatDate(context!.ExpiresAt)
</Template> </Template>
</CrudColumn> </MCBColumn>
<CrudColumn TItem="ApiKey" Field="@(x => x.PermissionJson)" Title="Permissions"/> <MCBColumn TItem="ApiKey" Field="@(x => x.PermissionJson)" Title="Permissions"/>
</View> </View>
</AutoCrud> </FastCrud>
</div> </div>
@code @code
{ {
private IEnumerable<ApiKey> ApiKeysLoader(Repository<ApiKey> repository) private void OnConfigure(FastCrudConfiguration<ApiKey> configuration)
{ {
return repository.Get(); configuration.ValidateCreate = async apiKey =>
} {
// TODO: Remove this when correct permission editor exists
if (string.IsNullOrEmpty(apiKey.PermissionJson))
apiKey.PermissionJson = "[]";
private async Task ValidateAdd(ApiKey apiKey)
{
var key = Formatter.GenerateString(32); var key = Formatter.GenerateString(32);
apiKey.Key = key; apiKey.Key = key;
await ClipboardService.Copy(key); await ClipboardService.Copy(key);
await ToastService.Info("Copied api key into your clipboard"); await ToastService.Info("Copied api key into your clipboard");
};
}
private void OnConfigureFrom(FastFormConfiguration<ApiKey> configuration, ApiKey _)
{
configuration.AddProperty(x => x.Description)
.WithDefaultComponent()
.WithDescription("Write a note here for which application the api key is used for")
.WithValidation(FastFormValidators.Required);
configuration.AddProperty(x => x.ExpiresAt)
.WithDefaultComponent()
.WithDescription("Specify when the api key should expire");
configuration.AddProperty(x => x.PermissionJson)
.WithDefaultComponent()
.WithName("Permissions");
} }
} }

View File

@@ -12,7 +12,7 @@
<div class="row mb-8 gap-4"> <div class="row mb-8 gap-4">
@foreach (var column in Columns.OrderBy(x => x.Index)) @foreach (var column in Columns.OrderBy(x => x.Index))
{ {
if (column.RequiredPermissionLevel <= IdentityService.CurrentUser.Permissions) if (column.RequiredPermissionLevel <= IdentityService.GetUser().Permissions)
{ {
<div class="col-12 col-lg-6 col-xl"> <div class="col-12 col-lg-6 col-xl">
@column.Component @column.Component
@@ -22,7 +22,7 @@
</div> </div>
@foreach (var component in Components.OrderBy(x => x.Index)) @foreach (var component in Components.OrderBy(x => x.Index))
{ {
if (component.RequiredPermissionLevel <= IdentityService.CurrentUser.Permissions) if (component.RequiredPermissionLevel <= IdentityService.GetUser().Permissions)
{ {
<div class="mb-4"> <div class="mb-4">
@component.Component @component.Component

View File

@@ -1,10 +1,9 @@
@page "/admin/sys/diagnose" @page "/admin/sys/diagnose"
@using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.UI.Components.Navigations
@using MoonCoreUI.Services
@using Moonlight.Core.Services @using Moonlight.Core.Services
@inject FileDownloadService DownloadService @inject DownloadService DownloadService
@inject DiagnoseService DiagnoseService @inject DiagnoseService DiagnoseService
@attribute [RequirePermission(9999)] @attribute [RequirePermission(9999)]
@@ -31,7 +30,7 @@
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<div class="card card-body p-5"> <div class="card card-body p-5">
<WButton OnClick="DownloadReport" Text="Download diagnose report" CssClasses="btn btn-primary"/> <WButton OnClick="DownloadReport" CssClasses="btn btn-primary">Download diagnose report</WButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@
@using MoonCore.Helpers @using MoonCore.Helpers
@inject MoonlightService MoonlightService @inject MoonlightService MoonlightService
@inject HostSystemHelper HostSystemHelper
@attribute [RequirePermission(9999)] @attribute [RequirePermission(9999)]
@@ -75,7 +76,7 @@
<i class="bx bx-lg text-white align-middle bx-refresh"></i> <i class="bx bx-lg text-white align-middle bx-refresh"></i>
</div> </div>
<div class="col-9 d-flex align-items-center"> <div class="col-9 d-flex align-items-center">
<ConfirmButton OnClick="MoonlightService.Restart" Text="Restart" CssClasses="btn btn-lg btn-danger w-100"/> <ConfirmButton OnClick="_ => MoonlightService.Restart()" CssClasses="btn btn-lg btn-danger w-100">Restart</ConfirmButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,6 @@
@using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.UI.Components.Navigations
@using MoonCore.Abstractions @using MoonCore.Abstractions
@using MoonCoreUI.Services
@using Moonlight.Core.Database.Entities @using Moonlight.Core.Database.Entities
@using Moonlight.Core.Services @using Moonlight.Core.Services
@@ -17,7 +16,7 @@
<LazyLoader Load="Load"> <LazyLoader Load="Load">
<div class="card card-body p-5"> <div class="card card-body p-5">
<div class="input-group"> <div class="input-group">
<SmartDropdown @bind-Value="SelectedUser" <MCBDropdown @bind-Value="SelectedUser"
Items="Users" Items="Users"
Placeholder="Select a user" Placeholder="Select a user"
DisplayFunc="@(x => x.Username)" DisplayFunc="@(x => x.Username)"
@@ -58,14 +57,7 @@
</div> </div>
<div class="card card-body p-5 d-flex justify-content-center mt-5"> <div class="card card-body p-5 d-flex justify-content-center mt-5">
@if (SelectedUser == null) <WButton OnClick="SavePermissions" CssClasses="btn btn-primary">Apply</WButton>
{
<button class="btn btn-primary disabled" disabled="">Apply</button>
}
else
{
<WButton OnClick="SavePermissions" CssClasses="btn btn-primary" Text="Apply"/>
}
</div> </div>
} }
</LazyLoader> </LazyLoader>

View File

@@ -1,10 +1,10 @@
@page "/admin/sys/settings" @page "/admin/sys/settings"
@using System.ComponentModel
@using System.Linq.Expressions
@using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.UI.Components.Navigations
@using System.Reflection @using System.Reflection
@using MoonCore.Services @using MoonCore.Services
@using MoonCoreUI.Services
@using MoonCoreUI.Helpers
@using Moonlight.Core.Configuration @using Moonlight.Core.Configuration
@inject ConfigService<CoreConfiguration> ConfigService @inject ConfigService<CoreConfiguration> ConfigService
@@ -14,7 +14,7 @@
<AdminSysNavigation Index="1"/> <AdminSysNavigation Index="1"/>
@if (ModelToShow == null) @if (CurrentModel == null)
{ {
<IconAlert Title="No resource to show" Icon="bx-x"> <IconAlert Title="No resource to show" Icon="bx-x">
No model found to show. Please refresh the page to go back No model found to show. Please refresh the page to go back
@@ -22,20 +22,36 @@
} }
else else
{ {
<div class="mt-5">
<Tooltip>
Changes to these settings are live applied. The save button only make the changes persistently saved to disk
</Tooltip>
</div>
<div class="card mt-5 mb-5"> <div class="card mt-5 mb-5">
<div class="card-header border-bottom-0"> <div class="card-header border-bottom-0">
@{ <h3 class="card-title">
string title; @if (Path.Length == 0)
{
if (Path.Length == 0) <span>Configuration</span>
title = "Configuration"; }
else else
{ {
title = "Configuration - " + string.Join(" - ", Path); <span class="text-muted">
} <span class="align-middle">Configuration</span>
}
<h3 class="card-title">@(title)</h3> @foreach (var subPart in Path.SkipLast(1))
{
<i class="bx bx-sm bx-chevron-right me-1 align-middle"></i>
<span class="align-middle">@subPart</span>
}
</span>
<span>
<i class="bx bx-sm bx-chevron-right align-middle text-muted"></i>
<span class="align-middle">@Path.Last()</span>
</span>
}
</h3>
<div class="card-toolbar"> <div class="card-toolbar">
<WButton OnClick="Reload" CssClasses="btn btn-icon btn-warning me-3"> <WButton OnClick="Reload" CssClasses="btn btn-icon btn-warning me-3">
<i class="bx bx-sm bx-revision"></i> <i class="bx bx-sm bx-revision"></i>
@@ -47,33 +63,23 @@ else
</div> </div>
</div> </div>
<Tooltip>
Changes to these settings are live applied. The save button only make the changes persistently saved to disk
</Tooltip>
<div class="row mt-5"> <div class="row mt-5">
<div class="col-md-3 col-12 mb-5"> <div class="col-md-3 col-12 mb-5">
<div class="card card-body"> <div class="card card-body">
@{ @foreach (var item in SidebarItems)
var props = ModelToShow
.GetType()
.GetProperties()
.Where(x => x.PropertyType.Assembly.FullName!.Contains("Moonlight") && x.PropertyType.IsClass)
.ToArray();
}
@foreach (var prop in props)
{ {
<div class="d-flex flex-stack"> <div class="d-flex flex-stack">
<div class="d-flex align-items-center flex-row-fluid flex-wrap"> <div class="d-flex align-items-center flex-row-fluid flex-wrap">
<a href="/admin/sys/settings?section=@(Section + "/" + prop.Name)" class="fs-4 text-primary">@(prop.Name)</a> <a href="/admin/sys/settings?section=@(Section + "/" + item)" class="fs-4 text-primary">
@item
</a>
</div> </div>
</div> </div>
} }
@if (Path.Length != 0) @if (Path.Length != 0)
{ {
<div class="d-flex flex-stack @(props.Length != 0 ? "mt-5" : "")"> <div class="d-flex flex-stack @(SidebarItems.Length != 0 ? "mt-5" : "")">
<div class="d-flex align-items-center flex-row-fluid flex-wrap"> <div class="d-flex align-items-center flex-row-fluid flex-wrap">
<a href="/admin/sys/@(GetBackPath())" class="fs-4 text-primary">Back</a> <a href="/admin/sys/@(GetBackPath())" class="fs-4 text-primary">Back</a>
</div> </div>
@@ -82,40 +88,21 @@ else
</div> </div>
</div> </div>
<div class="col-md-9 col-12"> <div class="col-md-9 col-12">
<div class="card card-body"> <LazyLoader @ref="LazyLoader" Load="Load" UseDefaultValues="false" TimeUntilSpinnerIsShown="TimeSpan.Zero">
<div class="row g-5"> <FastForm Model="CurrentModel" OnConfigure="OnFormConfigure"/>
<LazyLoader @ref="LazyLoader" Load="Load">
@foreach (var prop in Properties)
{
<div class="col-md-6 col-12">
@{
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
var rf = ComponentHelper.FromType(typeToCreate, parameters =>
{
parameters.Add("Data", ModelToShow);
parameters.Add("Property", prop);
});
}
@rf
</div>
}
</LazyLoader> </LazyLoader>
</div> </div>
</div> </div>
</div>
</div>
} }
@code @code
{ {
[Parameter] [Parameter] [SupplyParameterFromQuery] public string? Section { get; set; } = "";
[SupplyParameterFromQuery]
public string? Section { get; set; } = "";
private object? ModelToShow; private object? CurrentModel;
private PropertyInfo[] Properties = Array.Empty<PropertyInfo>(); private string[] SidebarItems = [];
private string[] Path = Array.Empty<string>(); private string[] Path = [];
private PropertyInfo[] Properties = [];
private LazyLoader? LazyLoader; private LazyLoader? LazyLoader;
@@ -124,21 +111,36 @@ else
if (Section != null && Section.StartsWith("/")) if (Section != null && Section.StartsWith("/"))
Section = Section.TrimStart('/'); Section = Section.TrimStart('/');
Path = Section != null ? Section.Split("/") : Array.Empty<string>(); Path = Section != null ? Section.Split("/") : [];
ModelToShow = Resolve(ConfigService.Get(), Path, 0); CurrentModel = Resolve(ConfigService.Get(), Path, 0);
if (ModelToShow != null) if (CurrentModel == null)
{ {
Properties = ModelToShow SidebarItems = [];
.GetType() Properties = [];
.GetProperties()
.Where(x => !x.PropertyType.Assembly.FullName!.Contains("Moonlight"))
.ToArray();
} }
else else
{ {
Properties = Array.Empty<PropertyInfo>(); var props = CurrentModel
.GetType()
.GetProperties()
.ToArray();
SidebarItems = props
.Where(x =>
x.PropertyType.IsClass &&
x.PropertyType.Namespace!.StartsWith("Moonlight")
)
.Select(x => x.Name)
.ToArray();
Properties = props
.Where(x =>
!x.PropertyType.Namespace.StartsWith("Moonlight") &&
DefaultComponentRegistry.Get(x.PropertyType) != null // Check if a component has been registered for that type
)
.ToArray();
} }
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
@@ -147,16 +149,38 @@ else
await LazyLoader.Reload(); await LazyLoader.Reload();
} }
private void OnFormConfigure(FastFormConfiguration<object> configuration)
{
if(CurrentModel == null) // This will technically never be true because of the ui logic
return;
foreach (var property in Properties)
{
var propConfig = configuration
.AddProperty(CreatePropertyAccessExpression(property))
.WithDefaultComponent();
var customAttributes = property.GetCustomAttributes(false);
if(customAttributes.Length == 0)
continue;
if (TryGetAttribute(customAttributes, out DisplayNameAttribute nameAttribute))
propConfig.WithName(nameAttribute.DisplayName);
if (TryGetAttribute(customAttributes, out DescriptionAttribute descriptionAttribute))
propConfig.WithDescription(descriptionAttribute.Description);
}
}
private string GetBackPath() private string GetBackPath()
{ {
if (Path.Length == 1) if (Path.Length == 1)
return "settings"; return "settings";
else
{
var path = string.Join('/', Path.Take(Path.Length - 1)).TrimEnd('/'); var path = string.Join('/', Path.Take(Path.Length - 1)).TrimEnd('/');
return $"settings?section={path}"; return $"settings?section={path}";
} }
}
private object? Resolve(object model, string[] path, int index) private object? Resolve(object model, string[] path, int index)
{ {
@@ -177,20 +201,60 @@ else
return Resolve(prop.GetValue(model)!, path, index + 1); return Resolve(prop.GetValue(model)!, path, index + 1);
} }
private Task Load(LazyLoader arg) private Task Load(LazyLoader _) => Task.CompletedTask; // Seems useless, it more or less is, but it shows a nice loading ui while the form changes
{
return Task.CompletedTask;
}
private async Task Save() private async Task Save() // Saves all changes to disk, all changes are live updated as the config service reference will be edited directly
{ {
ConfigService.Save(); ConfigService.Save();
await ToastService.Success("Successfully saved config to disk"); await ToastService.Success("Successfully saved config to disk");
} }
private async Task Reload() private async Task Reload() // This will also discard all unsaved changes
{ {
ConfigService.Reload(); ConfigService.Reload();
await ToastService.Info("Reloaded configuration from disk"); await ToastService.Info("Reloaded configuration from disk");
} }
// Building lambda expressions at runtime using reflection is nice ;3
public static Expression<Func<object, object?>> CreatePropertyAccessExpression(PropertyInfo property)
{
// Get the type that declares the property
Type declaringType = property.DeclaringType!;
// Create a parameter expression for the input object
ParameterExpression param = Expression.Parameter(typeof(object), "obj");
// Create an expression to cast the input object to the declaring type
UnaryExpression cast = Expression.Convert(param, declaringType);
// Create an expression to access the property
MemberExpression propertyAccess = Expression.Property(cast, property);
// Create an expression to cast the property value to object
UnaryExpression castResult = Expression.Convert(propertyAccess, typeof(object));
// Create the final lambda expression
Expression<Func<object, object?>> lambda = Expression.Lambda<Func<object, object?>>(
castResult, param);
return lambda;
}
// From MoonCore. TODO: Maybe provide this and the above function as mooncore helper
private bool TryGetAttribute<T>(object[] attributes, out T result) where T : Attribute
{
var searchType = typeof(T);
var attr = attributes
.FirstOrDefault(x => x.GetType() == searchType);
if (attr == null)
{
result = default!;
return false;
}
result = (attr as T)!;
return true;
}
} }

View File

@@ -1,13 +1,10 @@
@page "/admin/users" @page "/admin/users"
@using System.ComponentModel.DataAnnotations
@using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.UI.Components.Navigations
@using MoonCore.Abstractions
@using Moonlight.Core.Database.Entities @using Moonlight.Core.Database.Entities
@using BlazorTable
@using MoonCore.Exceptions @using MoonCore.Exceptions
@using MoonCoreUI.Services
@using Moonlight.Core.Models.Abstractions @using Moonlight.Core.Models.Abstractions
@using Moonlight.Core.Models.Forms.Users
@inject AlertService AlertService @inject AlertService AlertService
@inject IAuthenticationProvider AuthenticationProvider @inject IAuthenticationProvider AuthenticationProvider
@@ -16,55 +13,98 @@
<AdminUsersNavigation Index="0"/> <AdminUsersNavigation Index="0"/>
<AutoCrud TItem="User" <FastCrud TItem="User"
TCreateForm="CreateUserForm" Search="Search"
TUpdateForm="UpdateUserForm" OnConfigure="OnConfigure"
Loader="Load" OnConfigureCreate="OnConfigureCreate"
CustomAdd="Add" OnConfigureEdit="OnConfigureEdit">
ValidateUpdate="ValidateUpdate">
<View> <View>
<CrudColumn TItem="User" Field="@(x => x.Id)" Title="Id" Filterable="true"/> <MCBColumn TItem="User" Field="@(x => x.Id)" Title="Id" Filterable="true"/>
<CrudColumn TItem="User" Field="@(x => x.Email)" Title="Email" Filterable="true"/> <MCBColumn TItem="User" Field="@(x => x.Email)" Title="Email" Filterable="true"/>
<CrudColumn TItem="User" Field="@(x => x.Username)" Title="Username" Filterable="true"/> <MCBColumn TItem="User" Field="@(x => x.Username)" Title="Username" Filterable="true"/>
<CrudColumn TItem="User" Field="@(x => x.CreatedAt)" Title="Created at"/> <MCBColumn TItem="User" Field="@(x => x.CreatedAt)" Title="Created at"/>
</View> </View>
<UpdateActions> <EditToolbar>
<WButton OnClick="() => ChangePassword(context)" CssClasses="btn btn-info me-2"> <WButton OnClick="() => ChangePassword(context)" CssClasses="btn btn-info me-2">
<i class="bx bx-sm bxs-key"></i> <i class="bx bx-sm bxs-key"></i>
Change password Change password
</WButton> </WButton>
</UpdateActions> </EditToolbar>
</AutoCrud> </FastCrud>
@code @code
{ {
private IEnumerable<User> Load(Repository<User> repository)
{
return repository.Get();
}
private async Task ChangePassword(User user) private async Task ChangePassword(User user)
{ {
var newPassword = await AlertService.Text($"Enter a new password for {user.Username}", ""); await AlertService.Text($"Change password for '{user.Username}'", "Enter a new password for {user.Username}", async newPassword =>
{
// This handles empty and canceled input // This handles empty and canceled input
if (string.IsNullOrEmpty(newPassword)) if (string.IsNullOrEmpty(newPassword))
return; return;
await AuthenticationProvider.ChangePassword(user, newPassword); await AuthenticationProvider.ChangePassword(user, newPassword);
});
} }
private async Task Add(User user) private void OnConfigure(FastCrudConfiguration<User> configuration)
{
configuration.CustomCreate = async user =>
{ {
var result = await AuthenticationProvider.Register(user.Username, user.Email, user.Password); var result = await AuthenticationProvider.Register(user.Username, user.Email, user.Password);
if (result == null) if (result == null)
throw new DisplayException("An unknown error occured while creating user"); throw new DisplayException("An unknown error occured while creating user");
} };
// To notify the authentication provider before we update the data in the database, we call it here configuration.ValidateEdit = async user =>
private async Task ValidateUpdate(User user)
{ {
await AuthenticationProvider.ChangeDetails(user, user.Email, user.Username); await AuthenticationProvider.ChangeDetails(user, user.Email, user.Username);
};
}
private void OnConfigureCreate(FastFormConfiguration<User> configuration, User _)
{
configuration.AddProperty(x => x.Username)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithValidation(RegexValidator.Create("^[a-z][a-z0-9]*$", "Usernames can only contain lowercase characters and numbers and should not start with a number"))
.WithValidation(x => x.Length >= 6 ? ValidationResult.Success : new ValidationResult("The username is too short"))
.WithValidation(x => x.Length <= 20 ? ValidationResult.Success : new ValidationResult("The username cannot be longer than 20 characters"));
configuration.AddProperty(x => x.Email)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithValidation(RegexValidator.Create("^.+@.+$", "You need to enter a valid email address"));
configuration.AddProperty(x => x.Password)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithValidation(x => x.Length >= 8 ? ValidationResult.Success : new ValidationResult("The password must be at least 8 characters long"))
.WithValidation(x => x.Length <= 256 ? ValidationResult.Success : new ValidationResult("The password must not be longer than 256 characters"));
}
private void OnConfigureEdit(FastFormConfiguration<User> configuration, User currentUser)
{
configuration.AddProperty(x => x.Username)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithValidation(RegexValidator.Create("^[a-z][a-z0-9]*$", "Usernames can only contain lowercase characters and numbers and should not start with a number"))
.WithValidation(x => x.Length >= 6 ? ValidationResult.Success : new ValidationResult("The username is too short"))
.WithValidation(x => x.Length <= 20 ? ValidationResult.Success : new ValidationResult("The username cannot be longer than 20 characters"));
configuration.AddProperty(x => x.Email)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithValidation(RegexValidator.Create("^.+@.+$", "You need to enter a valid email address"));
configuration.AddProperty(x => x.Totp)
.WithComponent<SwitchComponent>()
.WithName("Two factor authentication")
.WithDescription("This toggles the use of the two factor authentication");
}
private IEnumerable<User> Search(IEnumerable<User> source, string term)
{
return source.Where(x => x.Username.Contains(term) || x.Email.Contains(term));
} }
} }

View File

@@ -3,7 +3,6 @@
@using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.UI.Components.Navigations
@using Moonlight.Core.Services @using Moonlight.Core.Services
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonCoreUI.Services
@using Moonlight.Core.Models @using Moonlight.Core.Models
@inject SessionService SessionService @inject SessionService SessionService
@@ -24,16 +23,18 @@
<div class="card mt-5"> <div class="card mt-5">
<div class="card-body px-6 py-4"> <div class="card-body px-6 py-4">
<LazyLoader Load="Load"> <LazyLoader Load="Load">
<CrudTable @ref="Table" <MCBTable @ref="Table"
TItem="Session" TItem="Session"
ItemSource="SessionService.Sessions" ItemSource="SessionService.Sessions"
PageSize="50"> PageSize="50">
<CrudColumn TItem="Session" Title="User" Field="@(x => x.CreatedAt)"> <MCBColumn TItem="Session" Title="User" Field="@(x => x.CreatedAt)">
<Template> <Template>
@if (context.IdentityService.IsLoggedIn) @if (context.IdentityService.IsAuthenticated)
{ {
<a target="_blank" href="/admin/users/view/@(context.IdentityService.CurrentUser.Id)"> var user = context.IdentityService.GetUser();
@(context.IdentityService.CurrentUser.Email)
<a target="_blank" href="/admin/users/view/@(user.Id)">
@(user.Email)
</a> </a>
} }
else else
@@ -41,35 +42,35 @@
<span>Guest</span> <span>Guest</span>
} }
</Template> </Template>
</CrudColumn> </MCBColumn>
<CrudColumn TItem="Session" Title="URL" Field="@(x => x.CreatedAt)"> <MCBColumn TItem="Session" Title="URL" Field="@(x => x.CreatedAt)">
<Template> <Template>
<a target="_blank" href="@(context.NavigationManager.Uri)"> <a target="_blank" href="@(context.NavigationManager.Uri)">
@(context.NavigationManager.Uri) @(context.NavigationManager.Uri)
</a> </a>
</Template> </Template>
</CrudColumn> </MCBColumn>
<CrudColumn TItem="Session" Title="Last activity" Field="@(x => x.UpdatedAt)" Filterable="true"> <MCBColumn TItem="Session" Title="Last activity" Field="@(x => x.UpdatedAt)" Filterable="true">
<Template> <Template>
<span>@(Formatter.FormatUptime(DateTime.UtcNow - context.UpdatedAt))</span> <span>@(Formatter.FormatUptime(DateTime.UtcNow - context.UpdatedAt))</span>
</Template> </Template>
</CrudColumn> </MCBColumn>
<CrudColumn TItem="Session" Title="Connected since" Field="@(x => x.CreatedAt)" Filterable="true"> <MCBColumn TItem="Session" Title="Connected since" Field="@(x => x.CreatedAt)" Filterable="true">
<Template> <Template>
<span>@(Formatter.FormatUptime(DateTime.UtcNow - context.CreatedAt))</span> <span>@(Formatter.FormatUptime(DateTime.UtcNow - context.CreatedAt))</span>
</Template> </Template>
</CrudColumn> </MCBColumn>
<CrudColumn TItem="Session" Title="Actions"> <MCBColumn TItem="Session" Title="Actions">
<Template> <Template>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<div class="btn btn-group"> <div class="btn btn-group">
<WButton OnClick="() => Message(context)" Text="Message" CssClasses="btn btn-primary"/> <WButton OnClick="() => Message(context)" CssClasses="btn btn-primary">Message</WButton>
<WButton OnClick="() => Redirect(context)" Text="Redirect" CssClasses="btn btn-warning"/> <WButton OnClick="() => Redirect(context)" CssClasses="btn btn-warning">Redirect</WButton>
</div> </div>
</div> </div>
</Template> </Template>
</CrudColumn> </MCBColumn>
</CrudTable> </MCBTable>
</LazyLoader> </LazyLoader>
</div> </div>
</div> </div>
@@ -77,7 +78,7 @@
@code @code
{ {
private CrudTable<Session>? Table; private MCBTable<Session>? Table;
private Timer? UpdateTimer; private Timer? UpdateTimer;
private Task Load(LazyLoader _) private Task Load(LazyLoader _)
@@ -93,8 +94,8 @@
private async Task Redirect(Session session) private async Task Redirect(Session session)
{ {
var url = await AlertService.Text("Enter the target url to redirect to"); await AlertService.Text("Redirect to", "Enter the target url to redirect to", async url =>
{
if (string.IsNullOrEmpty(url)) if (string.IsNullOrEmpty(url))
return; return;
@@ -108,12 +109,13 @@
{ {
await ToastService.Danger("Unable to redirect user. The user is probably no longer connect with moonlight"); await ToastService.Danger("Unable to redirect user. The user is probably no longer connect with moonlight");
} }
});
} }
private async Task Message(Session session) private async Task Message(Session session)
{ {
var message = await AlertService.Text("Enter the message you want to send"); await AlertService.Text("Send message", "Enter the message you want to send", async message =>
{
if (string.IsNullOrEmpty(message)) if (string.IsNullOrEmpty(message))
return; return;
@@ -127,6 +129,7 @@
{ {
await ToastService.Danger("Unable to send message. The user is probably no longer connect with moonlight"); await ToastService.Danger("Unable to send message. The user is probably no longer connect with moonlight");
} }
});
} }
public void Dispose() public void Dispose()

View File

@@ -9,7 +9,7 @@
<LazyLoader Load="Load"> <LazyLoader Load="Load">
@foreach (var component in Components.OrderBy(x => x.Index)) @foreach (var component in Components.OrderBy(x => x.Index))
{ {
if (component.RequiredPermissionLevel <= IdentityService.CurrentUser.Permissions) if (component.RequiredPermissionLevel <= IdentityService.GetUser().Permissions)
{ {
<div class="mb-4"> <div class="mb-4">
@component.Component @component.Component

View File

@@ -24,7 +24,13 @@ public class FileManagerFeature : MoonlightFeature
// //
var config = new ConfigService<CoreConfiguration>(PathBuilder.File("storage", "configs", "core.json")); var config = new ConfigService<CoreConfiguration>(PathBuilder.File("storage", "configs", "core.json"));
context.Builder.Services.AddSingleton(new JwtService<FileManagerJwtType>(config.Get().Security.Token));
context.Builder.Services.AddSingleton(
new JwtService<FileManagerJwtType>(
config.Get().Security.Token,
context.LoggerFactory.CreateLogger<JwtService<FileManagerJwtType>>()
)
);
context.AddAsset("FileManager", "js/filemanager.js"); context.AddAsset("FileManager", "js/filemanager.js");
context.AddAsset("FileManager", "editor/ace.css"); context.AddAsset("FileManager", "editor/ace.css");

View File

@@ -13,11 +13,13 @@ public class DownloadController : Controller
{ {
private readonly JwtService<FileManagerJwtType> JwtService; private readonly JwtService<FileManagerJwtType> JwtService;
private readonly SharedFileAccessService SharedFileAccessService; private readonly SharedFileAccessService SharedFileAccessService;
private readonly ILogger<DownloadController> Logger;
public DownloadController(JwtService<FileManagerJwtType> jwtService, SharedFileAccessService sharedFileAccessService) public DownloadController(JwtService<FileManagerJwtType> jwtService, SharedFileAccessService sharedFileAccessService, ILogger<DownloadController> logger)
{ {
JwtService = jwtService; JwtService = jwtService;
SharedFileAccessService = sharedFileAccessService; SharedFileAccessService = sharedFileAccessService;
Logger = logger;
} }
[HttpGet] [HttpGet]
@@ -25,7 +27,7 @@ public class DownloadController : Controller
{ {
if (name.Contains("..")) if (name.Contains(".."))
{ {
Logger.Warn($"A user tried to access a file via path transversal. Name: {name}"); Logger.LogWarning("A user tried to access a file via path transversal. Name: {name}", name);
return NotFound(); return NotFound();
} }

View File

@@ -13,13 +13,15 @@ public class UploadController : Controller
{ {
private readonly JwtService<FileManagerJwtType> JwtService; private readonly JwtService<FileManagerJwtType> JwtService;
private readonly SharedFileAccessService SharedFileAccessService; private readonly SharedFileAccessService SharedFileAccessService;
private readonly ILogger<UploadController> Logger;
public UploadController( public UploadController(
JwtService<FileManagerJwtType> jwtService, JwtService<FileManagerJwtType> jwtService,
SharedFileAccessService sharedFileAccessService) SharedFileAccessService sharedFileAccessService, ILogger<UploadController> logger)
{ {
JwtService = jwtService; JwtService = jwtService;
SharedFileAccessService = sharedFileAccessService; SharedFileAccessService = sharedFileAccessService;
Logger = logger;
} }
// The following method/api endpoint needs some explanation: // The following method/api endpoint needs some explanation:
@@ -57,7 +59,7 @@ public class UploadController : Controller
if (path.Contains("..")) if (path.Contains(".."))
{ {
Logger.Warn("A path transversal attack has been detected while processing upload path", "security"); Logger.LogWarning("A path transversal attack has been detected while processing upload path: {path}", path);
return BadRequest("Invalid path. This attempt has been logged ;)"); return BadRequest("Invalid path. This attempt has been logged ;)");
} }

View File

@@ -1,6 +1,6 @@
using MoonCore.Blazor.Services;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCoreUI.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
@@ -23,9 +23,9 @@ public class ArchiveContextAction : IFileManagerContextAction
var alertService = provider.GetRequiredService<AlertService>(); var alertService = provider.GetRequiredService<AlertService>();
var fileName = await alertService.Text("Enter the archive file name", "", await alertService.Text("Create an archive", "Enter the archive file name",
Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz"); async fileName =>
{
if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled
return; return;
@@ -44,16 +44,18 @@ public class ArchiveContextAction : IFileManagerContextAction
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn($"An error occured while archiving item ({entry.Name}):"); var logger = provider.GetRequiredService<ILogger<ArchiveContextAction>>();
Logger.Warn(e); logger.LogWarning("An error occured while archiving item ({name}): {e}", entry.Name, e);
await toastService.Danger("An unknown error occured while creating archive"); await toastService.Danger("An unknown error occured while creating archive");
} }
finally finally
{ {
await toastService.RemoveProgress("fileManagerArchive"); await toastService.DeleteProgress("fileManagerArchive");
} }
await fileManager.View.Refresh(); await fileManager.View.Refresh();
},
Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz");
} }
} }

View File

@@ -1,6 +1,6 @@
using MoonCore.Blazor.Services;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCoreUI.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
@@ -21,9 +21,8 @@ public class ArchiveSelectionAction : IFileManagerSelectionAction
var alertService = provider.GetRequiredService<AlertService>(); var alertService = provider.GetRequiredService<AlertService>();
var fileName = await alertService.Text("Enter the archive file name", "", await alertService.Text("Create an archive", "Enter the archive file name", async fileName =>
Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz"); {
if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled
return; return;
@@ -42,14 +41,16 @@ public class ArchiveSelectionAction : IFileManagerSelectionAction
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn($"An error occured while archiving items ({entries.Length}):"); var logger = provider.GetRequiredService<ILogger<ArchiveSelectionAction>>();
Logger.Warn(e); logger.LogWarning("An error occured while archiving items ({lenght}): {e}", entries.Length, e);
await toastService.Danger("An unknown error occured while creating archive"); await toastService.Danger("An unknown error occured while creating archive");
} }
finally finally
{ {
await toastService.RemoveProgress("fileManagerArchive"); await toastService.DeleteProgress("fileManagerArchive");
} }
},
Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz");
} }
} }

View File

@@ -1,4 +1,4 @@
using MoonCoreUI.Services; using MoonCore.Blazor.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
@@ -14,8 +14,8 @@ public class CreateFileAction : IFileManagerCreateAction
{ {
var alertService = provider.GetRequiredService<AlertService>(); var alertService = provider.GetRequiredService<AlertService>();
var name = await alertService.Text("Enter a name for the new file"); await alertService.Text("Create a new file","Enter a name for the new file", async name =>
{
if (string.IsNullOrEmpty(name) || name.Contains("..")) if (string.IsNullOrEmpty(name) || name.Contains(".."))
return; return;
@@ -30,5 +30,6 @@ public class CreateFileAction : IFileManagerCreateAction
IsDirectory = false, IsDirectory = false,
LastModifiedAt = DateTime.UtcNow LastModifiedAt = DateTime.UtcNow
}); });
});
} }
} }

View File

@@ -1,4 +1,4 @@
using MoonCoreUI.Services; using MoonCore.Blazor.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
using Moonlight.Features.FileManager.UI.Components; using Moonlight.Features.FileManager.UI.Components;
@@ -16,8 +16,8 @@ public class CreateFolderAction : IFileManagerCreateAction
var alertService = provider.GetRequiredService<AlertService>(); var alertService = provider.GetRequiredService<AlertService>();
var toastService = provider.GetRequiredService<ToastService>(); var toastService = provider.GetRequiredService<ToastService>();
var name = await alertService.Text("Enter a name for the new directory"); await alertService.Text("Create a new folder", "Enter a name for the new directory", async name =>
{
if (string.IsNullOrEmpty(name) || name.Contains("..")) if (string.IsNullOrEmpty(name) || name.Contains(".."))
return; return;
@@ -25,5 +25,6 @@ public class CreateFolderAction : IFileManagerCreateAction
await toastService.Success("Successfully created directory"); await toastService.Success("Successfully created directory");
await fileManager.View.Refresh(); await fileManager.View.Refresh();
});
} }
} }

View File

@@ -1,4 +1,5 @@
using MoonCoreUI.Services;
using MoonCore.Blazor.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
using Moonlight.Features.FileManager.UI.Components; using Moonlight.Features.FileManager.UI.Components;

View File

@@ -1,4 +1,5 @@
using MoonCoreUI.Services;
using MoonCore.Blazor.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
using Moonlight.Features.FileManager.UI.Components; using Moonlight.Features.FileManager.UI.Components;
@@ -44,21 +45,22 @@ public class DeleteSelectionAction : IFileManagerSelectionAction
fileList += "And " + (fileCount - showFileCount) + " more files..."; fileList += "And " + (fileCount - showFileCount) + " more files...";
if(!await alertService.YesNo($"Do you really want to delete {folderCount + fileCount} item(s)? \n\n" + fileList)) await alertService.Confirm("Confirm file deletion",
return; $"Do you really want to delete {folderCount + fileCount} item(s)? \n\n" + fileList, async () =>
{
await toastService.CreateProgress("fileManagerSelectionDelete", "Deleting items"); await toastService.CreateProgress("fileManagerSelectionDelete", "Deleting items");
foreach (var entry in entries) foreach (var entry in entries)
{ {
await toastService.ModifyProgress("fileManagerSelectionDelete", $"Deleting '{entry.Name}'"); await toastService.UpdateProgress("fileManagerSelectionDelete", $"Deleting '{entry.Name}'");
await access.Delete(entry); await access.Delete(entry);
} }
await toastService.RemoveProgress("fileManagerSelectionDelete"); await toastService.DeleteProgress("fileManagerSelectionDelete");
await toastService.Success("Successfully deleted selection"); await toastService.Success("Successfully deleted selection");
await fileManager.View.Refresh(); await fileManager.View.Refresh();
});
} }
} }

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MoonCore.Blazor.Services;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCoreUI.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
using Moonlight.Features.FileManager.Services; using Moonlight.Features.FileManager.Services;
@@ -15,11 +16,11 @@ public class DownloadContextAction : IFileManagerContextAction
public string Color => "primary"; public string Color => "primary";
public Func<FileEntry, bool> Filter => entry => entry.IsFile; public Func<FileEntry, bool> Filter => entry => entry.IsFile;
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider serviceProvider) public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider provider)
{ {
var fileAccessService = serviceProvider.GetRequiredService<SharedFileAccessService>(); var fileAccessService = provider.GetRequiredService<SharedFileAccessService>();
var navigation = serviceProvider.GetRequiredService<NavigationManager>(); var navigation = provider.GetRequiredService<NavigationManager>();
var toastService = serviceProvider.GetRequiredService<ToastService>(); var toastService = provider.GetRequiredService<ToastService>();
try try
{ {
@@ -31,8 +32,8 @@ public class DownloadContextAction : IFileManagerContextAction
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn("Unable to start download"); var logger = provider.GetRequiredService<ILogger<DownloadContextAction>>();
Logger.Warn(e); logger.LogWarning("Unable to start download: {e}", e); ;
await toastService.Danger("Failed to start download"); await toastService.Danger("Failed to start download");
} }

View File

@@ -1,6 +1,7 @@
using MoonCore.Blazor.Services;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCoreUI.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
@@ -36,14 +37,14 @@ public class ExtractContextAction : IFileManagerContextAction
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn($"An error occured while extracting archive ({entry.Name}):"); var logger = provider.GetRequiredService<ILogger<ExtractContextAction>>();
Logger.Warn(e); logger.LogWarning("An error occured while extracting archive ({name}): {e}", entry.Name, e);
await toastService.Danger("An unknown error occured while extracting archive"); await toastService.Danger("An unknown error occured while extracting archive");
} }
finally finally
{ {
await toastService.RemoveProgress("fileManagerExtract"); await toastService.DeleteProgress("fileManagerExtract");
} }
await fileManager.View.Refresh(); await fileManager.View.Refresh();

View File

@@ -1,4 +1,5 @@
using MoonCoreUI.Services;
using MoonCore.Blazor.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;

View File

@@ -1,4 +1,5 @@
using MoonCoreUI.Services;
using MoonCore.Blazor.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
@@ -19,12 +20,12 @@ public class MoveSelectionAction : IFileManagerSelectionAction
foreach (var entry in entries) foreach (var entry in entries)
{ {
await toastService.ModifyProgress("fileManagerSelectionMove", $"Moving '{entry.Name}'"); await toastService.UpdateProgress("fileManagerSelectionMove", $"Moving '{entry.Name}'");
await access.Move(entry, path + entry.Name); await access.Move(entry, path + entry.Name);
} }
await toastService.RemoveProgress("fileManagerSelectionMove"); await toastService.DeleteProgress("fileManagerSelectionMove");
await toastService.Success("Successfully moved selection"); await toastService.Success("Successfully moved selection");
await fileManager.View.Refresh(); await fileManager.View.Refresh();

View File

@@ -1,4 +1,5 @@
using MoonCoreUI.Services;
using MoonCore.Blazor.Services;
using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
using Moonlight.Features.FileManager.UI.Components; using Moonlight.Features.FileManager.UI.Components;
@@ -17,8 +18,8 @@ public class RenameContextAction : IFileManagerContextAction
var alertService = provider.GetRequiredService<AlertService>(); var alertService = provider.GetRequiredService<AlertService>();
var toastService = provider.GetRequiredService<ToastService>(); var toastService = provider.GetRequiredService<ToastService>();
var newName = await alertService.Text($"Enter a new name for '{entry.Name}'", "", entry.Name); await alertService.Text("Rename file" , $"Enter a new name for '{entry.Name}'", async newName =>
{
if (string.IsNullOrEmpty(newName)) if (string.IsNullOrEmpty(newName))
return; return;
@@ -26,5 +27,6 @@ public class RenameContextAction : IFileManagerContextAction
await fileManager.View.Refresh(); await fileManager.View.Refresh();
await toastService.Success("Successfully renamed file"); await toastService.Success("Successfully renamed file");
}, entry.Name);
} }
} }

View File

@@ -1,5 +1,6 @@
using Microsoft.JSInterop; using Microsoft.JSInterop;
using MoonCore.Attributes; using MoonCore.Attributes;
using MoonCore.Blazor.Extensions;
using MoonCore.Helpers; using MoonCore.Helpers;
namespace Moonlight.Features.FileManager.Services; namespace Moonlight.Features.FileManager.Services;
@@ -9,32 +10,30 @@ public class FileManagerInteropService
{ {
private readonly IJSRuntime JsRuntime; private readonly IJSRuntime JsRuntime;
public SmartEventHandler OnUploadStateChanged { get; set; } = new(); public SmartEventHandler OnUploadStateChanged { get; set; }
public FileManagerInteropService(IJSRuntime jsRuntime) public FileManagerInteropService(IJSRuntime jsRuntime, ILogger<SmartEventHandler> eventHandlerLogger)
{ {
JsRuntime = jsRuntime; JsRuntime = jsRuntime;
OnUploadStateChanged = new(eventHandlerLogger);
} }
public async Task InitDropzone(string id, string urlId) public async Task InitDropzone(string id, string urlId)
{ {
var reference = DotNetObjectReference.Create(this); var reference = DotNetObjectReference.Create(this);
await JsRuntime.InvokeVoidAsync("filemanager.dropzone.init", id, urlId, reference); await JsRuntime.InvokeVoidAsyncHandled("filemanager.dropzone.init", id, urlId, reference);
} }
public async Task InitFileSelect(string id, string urlId) public async Task InitFileSelect(string id, string urlId)
{ {
var reference = DotNetObjectReference.Create(this); var reference = DotNetObjectReference.Create(this);
await JsRuntime.InvokeVoidAsync("filemanager.fileselect.init", id, urlId, reference); await JsRuntime.InvokeVoidAsyncHandled("filemanager.fileselect.init", id, urlId, reference);
} }
public async Task UpdateUrl(string urlId, string url) public async Task UpdateUrl(string urlId, string url)
{ {
try await JsRuntime.InvokeVoidAsyncHandled("filemanager.updateUrl", urlId, url);
{
await JsRuntime.InvokeVoidAsync("filemanager.updateUrl", urlId, url);
}
catch (TaskCanceledException) { /* ignored */ }
} }
[JSInvokable] [JSInvokable]

View File

@@ -1,4 +1,3 @@
@using MoonCoreUI.Services
@using Moonlight.Core.Services @using Moonlight.Core.Services
@using MoonCore.Helpers @using MoonCore.Helpers
@using Moonlight.Core.Helpers @using Moonlight.Core.Helpers
@@ -7,6 +6,7 @@
@inject ToastService ToastService @inject ToastService ToastService
@inject HotKeyService HotKeyService @inject HotKeyService HotKeyService
@inject ILogger<FileEditor> Logger
@implements IDisposable @implements IDisposable
@@ -76,8 +76,7 @@
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn($"An unhandled error has occured while saving a file using access type {FileAccess.GetType().FullName}"); Logger.LogWarning("An unhandled error has occured while saving a file using access type {name}: {e}", FileAccess.GetType().FullName, e);
Logger.Warn(e);
await ToastService.Danger("An unknown error has occured while saving the file. Please try again later"); await ToastService.Danger("An unknown error has occured while saving the file. Please try again later");
return; return;

View File

@@ -1,6 +1,5 @@
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonCore.Services @using MoonCore.Services
@using MoonCoreUI.Services
@using Moonlight.Core.Configuration @using Moonlight.Core.Configuration
@using Moonlight.Core.Services @using Moonlight.Core.Services
@using Moonlight.Features.FileManager.Interfaces @using Moonlight.Features.FileManager.Interfaces
@@ -9,6 +8,7 @@
@inject AlertService AlertService @inject AlertService AlertService
@inject ToastService ToastService @inject ToastService ToastService
@inject ModalService ModalService
@inject FileManagerInteropService FileManagerInteropService @inject FileManagerInteropService FileManagerInteropService
@inject SharedFileAccessService FileAccessService @inject SharedFileAccessService FileAccessService
@inject ConfigService<CoreConfiguration> ConfigService @inject ConfigService<CoreConfiguration> ConfigService
@@ -111,28 +111,6 @@ else
</ContextMenuTemplate> </ContextMenuTemplate>
</FileView> </FileView>
</div> </div>
<SmartModal @ref="FolderSelectModal" CssClasses="modal-lg modal-dialog-centered">
<div class="modal-header">
<h5 class="modal-title">@FolderSelectTitle</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="HideFolderSelect"></button>
</div>
<div class="modal-body">
<FileView @ref="FolderSelectView"
FileAccess="FolderSelectFileAccess"
Filter="FolderSelectFilter"
ShowDate="false"
ShowSelect="false"
ShowSize="false"
OnEntryClicked="EntryClickFolderSelect"
OnNavigateUpClicked="NavigateUpFolderSelect"
EnableContextMenu="false"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="HideFolderSelect">Cancel</button>
<button type="button" class="btn btn-primary" @onclick="SubmitFolderSelect">Submit</button>
</div>
</SmartModal>
} }
@code @code
@@ -151,15 +129,6 @@ else
private FileEntry FileToEdit; private FileEntry FileToEdit;
private bool ShowEditor = false; private bool ShowEditor = false;
// Folder select dialog
private bool FolderSelectIsOpen = false;
private SmartModal FolderSelectModal;
private BaseFileAccess FolderSelectFileAccess;
private string FolderSelectTitle;
private Func<string, Task> FolderSelectResult;
private FileView FolderSelectView;
private Func<FileEntry, bool> FolderSelectFilter => entry => entry.IsDirectory;
private Timer? UploadTokenTimer; private Timer? UploadTokenTimer;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@@ -175,7 +144,6 @@ else
if (!firstRender) if (!firstRender)
return; return;
// Setup upload url update timer // Setup upload url update timer
UploadTokenTimer = new(async _ => UploadTokenTimer = new(async _ =>
{ {
@@ -308,52 +276,16 @@ else
#endregion #endregion
#region Selects
public async Task OpenFolderSelect(string title, Func<string, Task> onResult) public async Task OpenFolderSelect(string title, Func<string, Task> onResult)
{ {
if (FolderSelectIsOpen) await ModalService.Launch<FolderSelectModal>(cssClasses: "modal-lg modal-dialog-centered", buildAttributes: parameters =>
await HideFolderSelect();
FolderSelectResult = onResult;
FolderSelectTitle = title;
FolderSelectFileAccess = FileAccess.Clone();
await FolderSelectFileAccess.SetDirectory("/");
await FolderSelectModal.Show();
}
public async Task HideFolderSelect()
{ {
await FolderSelectModal.Hide(); parameters.Add("Title", title);
FolderSelectIsOpen = false; parameters.Add("OnResult", onResult);
FolderSelectFileAccess.Dispose(); parameters.Add("FileAccess", FileAccess.Clone());
});
} }
private async Task SubmitFolderSelect()
{
var path = await FolderSelectFileAccess.GetCurrentDirectory();
await HideFolderSelect();
await FolderSelectResult.Invoke(path);
}
private async Task NavigateUpFolderSelect()
{
await FolderSelectFileAccess.ChangeDirectory("..");
await FolderSelectView.Refresh();
}
private async Task EntryClickFolderSelect(FileEntry entry)
{
await FolderSelectFileAccess.ChangeDirectory(entry.Name);
await FolderSelectView.Refresh();
}
#endregion
public async void Dispose() public async void Dispose()
{ {
if (UploadTokenTimer != null) if (UploadTokenTimer != null)

View File

@@ -0,0 +1,61 @@
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
@implements IDisposable
<div class="modal-header">
<h5 class="modal-title">@Title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<FileView @ref="View"
FileAccess="FileAccess"
Filter="Filter"
ShowDate="false"
ShowSelect="false"
ShowSize="false"
OnEntryClicked="EntryClickFolderSelect"
OnNavigateUpClicked="NavigateUpFolderSelect"
EnableContextMenu="false"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" @onclick="SubmitFolderSelect">Submit</button>
</div>
@code
{
[Parameter] public BaseFileAccess FileAccess { get; set; }
[Parameter] public string Title { get; set; }
[Parameter] public Func<string, Task> OnResult { get; set; }
private FileView View;
private Func<FileEntry, bool> Filter => entry => entry.IsDirectory;
protected override async Task OnInitializedAsync()
{
await FileAccess.SetDirectory("/");
}
private async Task SubmitFolderSelect()
{
var path = await FileAccess.GetCurrentDirectory();
await OnResult.Invoke(path);
}
private async Task NavigateUpFolderSelect()
{
await FileAccess.ChangeDirectory("..");
await View.Refresh();
}
private async Task EntryClickFolderSelect(FileEntry entry)
{
await FileAccess.ChangeDirectory(entry.Name);
await View.Refresh();
}
public void Dispose()
{
FileAccess.Dispose();
}
}

View File

@@ -14,14 +14,14 @@ public class Server
public string? OverrideStartupCommand { get; set; } public string? OverrideStartupCommand { get; set; }
public int Cpu { get; set; } public int Cpu { get; set; } = 100;
public int Memory { get; set; } public int Memory { get; set; }
public int Disk { get; set; } public int Disk { get; set; }
public bool UseVirtualDisk { get; set; } public bool UseVirtualDisk { get; set; } = false;
public ServerNode Node { get; set; } public ServerNode Node { get; set; }
public ServerNetwork? Network { get; set; } public ServerNetwork? Network { get; set; }
public bool DisablePublicNetwork { get; set; } public bool DisablePublicNetwork { get; set; } = false;
public ServerAllocation MainAllocation { get; set; } public ServerAllocation MainAllocation { get; set; }
public List<ServerAllocation> Allocations { get; set; } = new(); public List<ServerAllocation> Allocations { get; set; } = new();

View File

@@ -3,7 +3,7 @@ namespace Moonlight.Features.Servers.Entities;
public class ServerAllocation public class ServerAllocation
{ {
public int Id { get; set; } public int Id { get; set; }
public string IpAddress { get; set; } = ""; public string IpAddress { get; set; } = "0.0.0.0";
public int Port { get; set; } public int Port { get; set; }
public string Note { get; set; } = ""; public string Note { get; set; } = "";
} }

View File

@@ -7,5 +7,5 @@ public class ServerDockerImage
public string DisplayName { get; set; } = ""; public string DisplayName { get; set; } = "";
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public bool AutoPull { get; set; } public bool AutoPull { get; set; } = true;
} }

View File

@@ -10,13 +10,13 @@ public class ServerImage
public string? UpdateUrl { get; set; } public string? UpdateUrl { get; set; }
public string? DonateUrl { get; set; } public string? DonateUrl { get; set; }
public string StartupCommand { get; set; } = ""; public string StartupCommand { get; set; } = "echo Startup command here";
public string OnlineDetection { get; set; } = ""; public string OnlineDetection { get; set; } = "Running";
public string StopCommand { get; set; } = ""; public string StopCommand { get; set; } = "^C";
public string InstallShell { get; set; } = ""; public string InstallShell { get; set; } = "/bin/bash";
public string InstallDockerImage { get; set; } = ""; public string InstallDockerImage { get; set; } = "debian:latest";
public string InstallScript { get; set; } = ""; public string InstallScript { get; set; } = "#! /bin/bash\necho Done";
public string ParseConfiguration { get; set; } = "[]"; public string ParseConfiguration { get; set; } = "[]";
public int AllocationsNeeded { get; set; } = 1; public int AllocationsNeeded { get; set; } = 1;

View File

@@ -1,9 +1,16 @@
using MoonCore.Helpers; using MoonCore.Attributes;
using MoonCore.Helpers;
using Moonlight.Features.Servers.Entities; using Moonlight.Features.Servers.Entities;
namespace Moonlight.Features.Servers.Events; namespace Moonlight.Features.Servers.Events;
[Singleton]
public class ServerEvents public class ServerEvents
{ {
public static SmartEventHandler<(Server, ServerBackup)> OnBackupCompleted { get; set; } = new(); public SmartEventHandler<(Server, ServerBackup)> OnBackupCompleted { get; set; }
public ServerEvents(ILogger<SmartEventHandler> eventHandlerLogger)
{
OnBackupCompleted = new(eventHandlerLogger);
}
} }

View File

@@ -16,8 +16,13 @@ public static class NodeExtensions
return new HttpApiClient<NodeException>(remoteUrl, node.Token); return new HttpApiClient<NodeException>(remoteUrl, node.Token);
} }
public static JwtService<ServersJwtType> CreateJwtService(this ServerNode node) public static JwtService<ServersJwtType> CreateJwtService(this ServerNode node, ILoggerFactory factory)
{ {
return new JwtService<ServersJwtType>(node.Token); return node.CreateJwtService(factory.CreateLogger<JwtService<ServersJwtType>>());
}
public static JwtService<ServersJwtType> CreateJwtService(this ServerNode node, ILogger<JwtService<ServersJwtType>> logger)
{
return new JwtService<ServersJwtType>(node.Token, logger);
} }
} }

View File

@@ -8,10 +8,10 @@ namespace Moonlight.Features.Servers.Helpers;
public class ServerConsole : IDisposable public class ServerConsole : IDisposable
{ {
public SmartEventHandler<ServerState> OnStateChange { get; set; } = new(); public SmartEventHandler<ServerState> OnStateChange { get; set; }
public SmartEventHandler<ServerStats> OnStatsChange { get; set; } = new(); public SmartEventHandler<ServerStats> OnStatsChange { get; set; }
public SmartEventHandler<string> OnNewMessage { get; set; } = new(); public SmartEventHandler<string> OnNewMessage { get; set; }
public SmartEventHandler OnDisconnected { get; set; } = new(); public SmartEventHandler OnDisconnected { get; set; }
public ServerState State { get; private set; } = ServerState.Offline; public ServerState State { get; private set; } = ServerState.Offline;
public ServerStats Stats { get; private set; } = new(); public ServerStats Stats { get; private set; } = new();
@@ -20,18 +20,29 @@ public class ServerConsole : IDisposable
private readonly List<string> MessageCache = new(); private readonly List<string> MessageCache = new();
private readonly Server Server; private readonly Server Server;
private readonly ILogger<ServerConsole> Logger;
private readonly ILogger<AdvancedWebsocketStream> AwsLogger;
private ClientWebSocket WebSocket; private ClientWebSocket WebSocket;
private AdvancedWebsocketStream WebsocketStream; private AdvancedWebsocketStream WebsocketStream;
private CancellationTokenSource Cancellation = new(); private CancellationTokenSource Cancellation = new();
public ServerConsole(Server server) public ServerConsole(Server server, ILoggerFactory loggerFactory)
{ {
if (server.Node == null) if (server.Node == null)
throw new ArgumentNullException(nameof(server.Node)); throw new ArgumentNullException(nameof(server.Node));
Server = server; Server = server;
Logger = loggerFactory.CreateLogger<ServerConsole>();
AwsLogger = loggerFactory.CreateLogger<AdvancedWebsocketStream>();
var eventHandlerLogger = loggerFactory.CreateLogger<SmartEventHandler>();
OnStateChange = new(eventHandlerLogger);
OnStatsChange = new(eventHandlerLogger);
OnDisconnected = new(eventHandlerLogger);
OnNewMessage = new(eventHandlerLogger);
} }
public async Task Connect() public async Task Connect()
@@ -49,7 +60,7 @@ public class ServerConsole : IDisposable
wsUrl = $"ws://{Server.Node.Fqdn}:{Server.Node.HttpPort}/servers/{Server.Id}/ws"; wsUrl = $"ws://{Server.Node.Fqdn}:{Server.Node.HttpPort}/servers/{Server.Id}/ws";
await WebSocket.ConnectAsync(new Uri(wsUrl), CancellationToken.None); await WebSocket.ConnectAsync(new Uri(wsUrl), CancellationToken.None);
WebsocketStream = new AdvancedWebsocketStream(WebSocket); WebsocketStream = new AdvancedWebsocketStream(AwsLogger, WebSocket);
WebsocketStream.RegisterPacket<string>(1); WebsocketStream.RegisterPacket<string>(1);
WebsocketStream.RegisterPacket<ServerState>(2); WebsocketStream.RegisterPacket<ServerState>(2);
@@ -103,11 +114,10 @@ public class ServerConsole : IDisposable
break; break;
if (e is WebSocketException) if (e is WebSocketException)
Logger.Warn($"Lost connection to daemon server websocket: {e.Message}"); Logger.LogWarning("Lost connection to daemon server websocket: {message}", e.Message);
else else
{ {
Logger.Warn("Server console ws disconnected because of application error:"); Logger.LogWarning("Server console ws disconnected because of application error: {e}", e);
Logger.Warn(e);
} }
break; break;

View File

@@ -18,11 +18,17 @@ public class ServersController : Controller
{ {
private readonly Repository<Server> ServerRepository; private readonly Repository<Server> ServerRepository;
private readonly Repository<ServerBackup> BackupRepository; private readonly Repository<ServerBackup> BackupRepository;
private readonly ILogger<ServersController> Logger;
private readonly ILogger<AdvancedWebsocketStream> WebSocketLogger;
private readonly ServerEvents ServerEvents;
public ServersController(Repository<Server> serverRepository, Repository<ServerBackup> backupRepository) public ServersController(Repository<Server> serverRepository, Repository<ServerBackup> backupRepository, ILogger<ServersController> logger, ILogger<AdvancedWebsocketStream> webSocketLogger, ServerEvents serverEvents)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
BackupRepository = backupRepository; BackupRepository = backupRepository;
Logger = logger;
WebSocketLogger = webSocketLogger;
ServerEvents = serverEvents;
} }
[HttpGet("ws")] [HttpGet("ws")]
@@ -36,7 +42,7 @@ public class ServersController : Controller
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
// Build connection wrapper // Build connection wrapper
var websocketStream = new AdvancedWebsocketStream(websocket); var websocketStream = new AdvancedWebsocketStream(WebSocketLogger, websocket);
websocketStream.RegisterPacket<int>(1); websocketStream.RegisterPacket<int>(1);
websocketStream.RegisterPacket<ServerConfiguration>(2); websocketStream.RegisterPacket<ServerConfiguration>(2);
@@ -66,8 +72,7 @@ public class ServersController : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error($"An error occured while sending server {server.Id} (Image: {server.Image.Name}) to daemon. This may indicate a corrupt or broken image/server. Skipping this server"); Logger.LogError("An error occured while sending server {serverId} (Image: {name}) to daemon. This may indicate a corrupt or broken image/server. Skipping this server. Error: {e}", server.Id, server.Image.Name, e);
Logger.Error(e);
} }
} }
@@ -146,7 +151,7 @@ public class ServersController : Controller
return NotFound(); return NotFound();
if(!status.Successful) if(!status.Successful)
Logger.Warn($"A node reported an error for a backup for the server {server.Id}"); Logger.LogWarning("A node reported an error for a backup for the server {serverId}", server.Id);
backup.Successful = status.Successful; backup.Successful = status.Successful;
backup.Completed = true; backup.Completed = true;

View File

@@ -1,9 +1,9 @@
using MoonCoreUI.Helpers; using MoonCore.Blazor.Helpers;
using Moonlight.Core.Interfaces.Ui.Admin; using Moonlight.Core.Interfaces.Ui.Admin;
using Moonlight.Core.Models.Abstractions; using Moonlight.Core.Models.Abstractions;
using Moonlight.Features.Servers.UI.Components.Cards; using Moonlight.Features.Servers.UI.Components.Cards;
namespace Moonlight.Features.Servers.Implementations.UI.Admin.AdminColumns; namespace Moonlight.Features.Servers.Implementations.AdminDashboard.Columns;
public class ServerCount : IAdminDashboardColumn public class ServerCount : IAdminDashboardColumn
{ {

View File

@@ -1,9 +1,9 @@
using MoonCoreUI.Helpers; using MoonCore.Blazor.Helpers;
using Moonlight.Core.Interfaces.Ui.Admin; using Moonlight.Core.Interfaces.Ui.Admin;
using Moonlight.Core.Models.Abstractions; using Moonlight.Core.Models.Abstractions;
using Moonlight.Features.Servers.UI.Components.Cards; using Moonlight.Features.Servers.UI.Components.Cards;
namespace Moonlight.Features.Servers.Implementations.UI.Admin.AdminComponents; namespace Moonlight.Features.Servers.Implementations.AdminDashboard.Components;
public class NodeOverview : IAdminDashboardComponent public class NodeOverview : IAdminDashboardComponent
{ {

View File

@@ -1,9 +1,8 @@
using MoonCoreUI.Helpers; using MoonCore.Blazor.Helpers;
using Moonlight.Core.Interfaces.UI.User; using Moonlight.Core.Interfaces.UI.User;
using Moonlight.Core.Models.Abstractions; using Moonlight.Core.Models.Abstractions;
using Moonlight.Features.Servers.UI.Components.Cards;
namespace Moonlight.Features.Servers.Implementations.UI.UserDashboard.Components; namespace Moonlight.Features.Servers.Implementations.UserDashboard.Components;
public class UserDashboardServerCount : IUserDashboardComponent public class UserDashboardServerCount : IUserDashboardComponent
{ {

View File

@@ -1,6 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using MoonCoreUI.Attributes; using MoonCore.Blazor.Attributes.Auto;
using Moonlight.Core.Database.Entities; using Moonlight.Core.Database.Entities;
using Moonlight.Features.Servers.Entities; using Moonlight.Features.Servers.Entities;
@@ -12,11 +12,11 @@ public class CreateServerForm
public string Name { get; set; } public string Name { get; set; }
[Required(ErrorMessage = "You need to specify a server owner")] [Required(ErrorMessage = "You need to specify a server owner")]
[Selector(SelectorProp = "Username", DisplayProp = "Username", UseDropdown = true)] //[Selector(SelectorProp = "Username", DisplayProp = "Username", UseDropdown = true)]
public User Owner { get; set; } public User Owner { get; set; }
[Required(ErrorMessage = "You need to specify a server image")] [Required(ErrorMessage = "You need to specify a server image")]
[Selector(SelectorProp = "Name", DisplayProp = "Name", UseDropdown = true)] //[Selector(SelectorProp = "Name", DisplayProp = "Name", UseDropdown = true)]
public ServerImage Image { get; set; } public ServerImage Image { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "Enter a valid cpu value")] [Range(1, int.MaxValue, ErrorMessage = "Enter a valid cpu value")]
@@ -26,31 +26,31 @@ public class CreateServerForm
[Range(1, int.MaxValue, ErrorMessage = "Enter a valid memory value")] [Range(1, int.MaxValue, ErrorMessage = "Enter a valid memory value")]
[Description("The amount of memory this server will be able to use")] [Description("The amount of memory this server will be able to use")]
[ByteSize(MinimumUnit = 1, Converter = 1, DefaultUnit = 2)] //[ByteSize(MinimumUnit = 1, Converter = 1, DefaultUnit = 2)]
[Section("Resources", Icon = "bxs-chip")] [Section("Resources", Icon = "bxs-chip")]
public int Memory { get; set; } public int Memory { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "Enter a valid disk value")] [Range(1, int.MaxValue, ErrorMessage = "Enter a valid disk value")]
[Description("The amount of disk space this server will be able to use")] [Description("The amount of disk space this server will be able to use")]
[ByteSize(MinimumUnit = 1, Converter = 1, DefaultUnit = 2)] //[ByteSize(MinimumUnit = 1, Converter = 1, DefaultUnit = 2)]
[Section("Resources", Icon = "bxs-chip")] [Section("Resources", Icon = "bxs-chip")]
public int Disk { get; set; } public int Disk { get; set; }
[Description("Whether to use a virtual disk for storing server files. Dont use this if you want to overallocate as the virtual disks will fill out the space you allocate")] [Description("Whether to use a virtual disk for storing server files. Dont use this if you want to overallocate as the virtual disks will fill out the space you allocate")]
[Section("Deployment", Icon = "bx-cube")] [Section("Deployment", Icon = "bx-cube")]
[RadioButtonBool("Virtual Disk", "Simple Volume", TrueIcon = "bxs-hdd", FalseIcon = "bxs-data")] //[RadioButtonBool("Virtual Disk", "Simple Volume", TrueIcon = "bxs-hdd", FalseIcon = "bxs-data")]
[DisplayName("Storage")] [DisplayName("Storage")]
public bool UseVirtualDisk { get; set; } public bool UseVirtualDisk { get; set; }
[Required(ErrorMessage = "You need to specify a server node")] [Required(ErrorMessage = "You need to specify a server node")]
[Selector(SelectorProp = "Name", DisplayProp = "Name", UseDropdown = true)] //[Selector(SelectorProp = "Name", DisplayProp = "Name", UseDropdown = true)]
[Section("Deployment", Icon = "bx-cube")] [Section("Deployment", Icon = "bx-cube")]
public ServerNode Node { get; set; } public ServerNode Node { get; set; }
[Description("The allocations the server should have")] [Description("The allocations the server should have")]
[MultiSelection("Port", "Port", Icon = "bx-network-chart")] //TODO: [MultiSelection("Port", "Port", Icon = "bx-network-chart")]
[Section("Deployment", Icon = "bx-cube")] [Section("Deployment", Icon = "bx-cube")]
[CustomItemLoader("FreeAllocations")] //[CustomItemLoader("FreeAllocations")]
[CustomDisplayFunction("AllocationWithIp")] //[CustomDisplayFunction("AllocationWithIp")]
public List<ServerAllocation> Allocations { get; set; } = new(); public List<ServerAllocation> Allocations { get; set; } = new();
} }

View File

@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using MoonCoreUI.Attributes;
using Moonlight.Features.Servers.Entities; using Moonlight.Features.Servers.Entities;
namespace Moonlight.Features.Servers.Models.Forms.Users.Networks; namespace Moonlight.Features.Servers.Models.Forms.Users.Networks;
@@ -10,6 +9,6 @@ public class CreateNetworkForm
public string Name { get; set; } public string Name { get; set; }
[Required(ErrorMessage = "You need to specify a node to create the network on")] [Required(ErrorMessage = "You need to specify a node to create the network on")]
[Selector(SelectorProp = "Name", DisplayProp = "Name")] //[Selector(SelectorProp = "Name", DisplayProp = "Name")]
public ServerNode Node { get; set; } public ServerNode Node { get; set; }
} }

View File

@@ -9,13 +9,13 @@ using Moonlight.Core.Services;
using Moonlight.Features.Servers.Actions; using Moonlight.Features.Servers.Actions;
using Moonlight.Features.Servers.Configuration; using Moonlight.Features.Servers.Configuration;
using Moonlight.Features.Servers.Http.Middleware; using Moonlight.Features.Servers.Http.Middleware;
using Moonlight.Features.Servers.Implementations.AdminDashboard.Columns;
using Moonlight.Features.Servers.Implementations.AdminDashboard.Components;
using Moonlight.Features.Servers.Implementations.Diagnose; using Moonlight.Features.Servers.Implementations.Diagnose;
using Moonlight.Features.Servers.Implementations.UI.Admin.AdminColumns;
using Moonlight.Features.Servers.Implementations.UI.Admin.AdminComponents;
using Moonlight.Features.Servers.Models.Enums; using Moonlight.Features.Servers.Models.Enums;
using Moonlight.Features.Servers.Services; using Moonlight.Features.Servers.Services;
using Moonlight.Features.Servers.UI.Components.Cards; using Moonlight.Features.Servers.UI.Components.Cards;
using UserDashboardServerCount = Moonlight.Features.Servers.Implementations.UI.UserDashboard.Components.UserDashboardServerCount; using UserDashboardServerCount = Moonlight.Features.Servers.Implementations.UserDashboard.Components.UserDashboardServerCount;
namespace Moonlight.Features.Servers; namespace Moonlight.Features.Servers;
@@ -34,7 +34,7 @@ public class ServersFeature : MoonlightFeature
// //
var config = new ConfigService<CoreConfiguration>(PathBuilder.File("storage", "configs", "core.json")); var config = new ConfigService<CoreConfiguration>(PathBuilder.File("storage", "configs", "core.json"));
context.Builder.Services.AddSingleton(new JwtService<ServersJwtType>(config.Get().Security.Token)); context.Builder.Services.AddSingleton(new JwtService<ServersJwtType>(config.Get().Security.Token, context.LoggerFactory.CreateLogger<JwtService<ServersJwtType>>()));
// //
var configService = new ConfigService<ServersConfiguration>(PathBuilder.File("storage", "configs", "servers.json")); var configService = new ConfigService<ServersConfiguration>(PathBuilder.File("storage", "configs", "servers.json"));

View File

@@ -11,10 +11,12 @@ namespace Moonlight.Features.Servers.Services;
public class NodeService public class NodeService
{ {
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
private readonly ILogger<NodeService> Logger;
public NodeService(IServiceProvider serviceProvider) public NodeService(IServiceProvider serviceProvider, ILogger<NodeService> logger)
{ {
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
Logger = logger;
} }
public async Task Boot(ServerNode node) public async Task Boot(ServerNode node)
@@ -42,8 +44,7 @@ public class NodeService
{ {
//TODO: Add http exception check to reduce error logs //TODO: Add http exception check to reduce error logs
Logger.Warn($"An error occured while booting node '{node.Name}'"); Logger.LogWarning("An error occured while booting node '{name}': {e}", node.Name, e);
Logger.Warn(e);
} }
}); });
} }

View File

@@ -116,7 +116,8 @@ public class ServerBackupService
var remoteUrl = $"{protocol}://{node.Fqdn}:{node.HttpPort}/"; var remoteUrl = $"{protocol}://{node.Fqdn}:{node.HttpPort}/";
// Build jwt // Build jwt
var jwtService = node.CreateJwtService(); var loggerFactory = ServiceProvider.GetRequiredService<ILoggerFactory>();
var jwtService = node.CreateJwtService(loggerFactory);
var jwt = await jwtService.Create(data => var jwt = await jwtService.Create(data =>
{ {

View File

@@ -14,10 +14,12 @@ public class ServerScheduleService
{ {
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
public readonly Dictionary<string, ScheduleAction> Actions = new(); public readonly Dictionary<string, ScheduleAction> Actions = new();
private readonly ILogger<ServerScheduleService> Logger;
public ServerScheduleService(IServiceProvider serviceProvider) public ServerScheduleService(IServiceProvider serviceProvider, ILogger<ServerScheduleService> logger)
{ {
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
Logger = logger;
} }
public Task RegisterAction<T>(string id) where T : ScheduleAction public Task RegisterAction<T>(string id) where T : ScheduleAction
@@ -50,7 +52,7 @@ public class ServerScheduleService
{ {
if (!Actions.ContainsKey(scheduleItem.Action)) if (!Actions.ContainsKey(scheduleItem.Action))
{ {
Logger.Warn($"The server {server.Id} has a invalid action type '{scheduleItem.Action}'"); Logger.LogWarning("The server {serverId} has a invalid action type '{action}'", server.Id, scheduleItem.Action);
continue; continue;
} }
@@ -69,8 +71,7 @@ public class ServerScheduleService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn($"An unhandled error occured while running schedule {schedule.Name} for server {server.Id}"); Logger.LogWarning("An unhandled error occured while running schedule {name} for server {serverId}: {e}", schedule.Name, server.Id, e);
Logger.Warn(e);
sw.Stop(); sw.Stop();

View File

@@ -27,10 +27,12 @@ public class ServerService
public NodeService NodeService => ServiceProvider.GetRequiredService<NodeService>(); public NodeService NodeService => ServiceProvider.GetRequiredService<NodeService>();
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
private readonly ILogger<ServerService> Logger;
public ServerService(IServiceProvider serviceProvider) public ServerService(IServiceProvider serviceProvider, ILogger<ServerService> logger)
{ {
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
Logger = logger;
} }
public async Task Sync(Server server) public async Task Sync(Server server)
@@ -84,8 +86,7 @@ public class ServerService
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn($"Could not establish to the node with the id {node.Id}"); Logger.LogWarning("Could not establish to the node with the id {nodeId}: {e}", node.Id, e);
Logger.Warn(e);
throw new DisplayException($"Could not establish connection to the node: {e.Message}"); throw new DisplayException($"Could not establish connection to the node: {e.Message}");
} }

View File

@@ -32,8 +32,8 @@
{ {
if (firstRender) if (firstRender)
{ {
ServerCount = await ServerRepository.Get().Where(x => x.Owner == IdentityService.CurrentUser).CountAsync(); ServerCount = await ServerRepository.Get().Where(x => x.Owner == IdentityService.GetUser()).CountAsync();
NetworksCount = await NetworkRepository.Get().Where(x => x.User == IdentityService.CurrentUser).CountAsync(); NetworksCount = await NetworkRepository.Get().Where(x => x.User == IdentityService.GetUser()).CountAsync();
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
} }

View File

@@ -1,6 +1,6 @@
@using XtermBlazor @using XtermBlazor
@using MoonCoreUI.Services
@inject ClipboardService ClipboardService @inject ClipboardService ClipboardService
@inject ToastService ToastService @inject ToastService ToastService

View File

@@ -1,6 +1,5 @@
@using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Entities
@using MoonCore.Abstractions @using MoonCore.Abstractions
@using MoonCoreUI.Services
@inject Repository<ServerVariable> ServerVariableRepository @inject Repository<ServerVariable> ServerVariableRepository
@inject ToastService ToastService @inject ToastService ToastService

View File

@@ -1,6 +1,6 @@
@using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Entities
@using MoonCore.Abstractions @using MoonCore.Abstractions
@using MoonCoreUI.Services
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using MoonCore.Helpers @using MoonCore.Helpers

View File

@@ -1,6 +1,6 @@
@using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Entities
@using MoonCore.Abstractions @using MoonCore.Abstractions
@using MoonCoreUI.Services
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@inject Repository<ServerVariable> ServerVariableRepository @inject Repository<ServerVariable> ServerVariableRepository

View File

@@ -1,6 +1,6 @@
@using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Entities
@using MoonCore.Abstractions @using MoonCore.Abstractions
@using MoonCoreUI.Services
@inject Repository<ServerVariable> ServerVariableRepository @inject Repository<ServerVariable> ServerVariableRepository
@inject ToastService ToastService @inject ToastService ToastService

View File

@@ -0,0 +1,51 @@
@using Microsoft.CSharp.RuntimeBinder
@using Moonlight.Features.Servers.Entities
@inherits FastFormBaseComponent<int>
<div class="col-md-6 col-12">
<label class="form-label">@Name</label>
<MCBSelect TField="ServerDockerImage"
@bind-Value="SelectedDockerImage"
Items="SortedImages"
DisplayField="@(x => x.Name)"/>
</div>
@code
{
[Parameter] public ServerImage Image { get; set; }
private ServerDockerImage? SelectedDockerImage
{
get
{
if (Binder.Value >= SortedImages.Count)
return null;
if (Binder.Value == -1)
return null;
return SortedImages[Binder.Value];
}
set
{
if (value == null)
{
Binder.Value = -1;
return;
}
Binder.Value = SortedImages.IndexOf(value);
}
}
private List<ServerDockerImage> SortedImages;
protected override void OnInitialized()
{
SortedImages = Image.DockerImages
.OrderBy(x => x.Id)
.ToList();
}
}

View File

@@ -0,0 +1,28 @@
@using Moonlight.Features.FileManager.UI.Components
@inherits FastFormBaseComponent<string>
<div class="col-12">
<label class="form-label">
@Name
</label>
<Editor InitialContent="@Binder.Value"
Mode="@Mode"
Lines="@Lines"
EnableAutoInit="true"
OnChanged="OnValueChanged"/>
</div>
@code
{
[Parameter] public string Mode { get; set; } = "sh";
[Parameter] public int Lines { get; set; } = 25;
private Task OnValueChanged(string val)
{
Binder.Value = val;
return Task.CompletedTask;
}
}

View File

@@ -1,8 +1,9 @@
@using Moonlight.Features.Servers.Models.Forms.Admin.Images @using Moonlight.Features.Servers.Models.Forms.Admin.Images
<div class="mt-5 card card-body p-8"> <div class="mt-5 card card-body p-8">
<div class="row g-5"> <div class="row g-5">@*
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<input class="form-control"/>
<SingleAutoProperty TItem="UpdateImageDetailedForm" <SingleAutoProperty TItem="UpdateImageDetailedForm"
Model="Form" Model="Form"
Field="@(x => x.Name)"/> Field="@(x => x.Name)"/>
@@ -21,7 +22,7 @@
<SingleAutoProperty TItem="UpdateImageDetailedForm" <SingleAutoProperty TItem="UpdateImageDetailedForm"
Model="Form" Model="Form"
Field="@(x => x.UpdateUrl)"/> Field="@(x => x.UpdateUrl)"/>
</div> </div>*@
</div> </div>
</div> </div>

View File

@@ -1,40 +1,20 @@
@using Moonlight.Features.Servers.Models.Forms.Admin.Images.DockerImages @using MoonCore.Abstractions
@using Microsoft.AspNetCore.Components.Forms @using MoonCore.Exceptions
@using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Entities
@using Moonlight.Features.Servers.Models.Forms.Admin.Images
<div class="card mb-10"> @inject Repository<ServerImage> ImageRepository
<div class="card-body"> @inject Repository<ServerDockerImage> DockerImageRepository
<div class="row g-8">
<div class="col-md-6 col-12">
<label class="form-label">Default docker image</label>
<SmartSelect TField="ServerDockerImage"
@bind-Value="SelectedDockerImage"
Items="Image.DockerImages"
DisplayField="@(x => x.Name)"
CanBeNull="true"/>
</div>
<div class="col-md-6 col-12">
<label class="form-label">Allow user to change the docker image</label>
<div class="form-check">
<InputCheckbox @bind-Value="Form.AllowDockerImageChange" class="form-check-input"/>
</div>
</div>
</div>
</div>
</div>
<AutoListCrud TItem="ServerDockerImage" <FastCrud TItem="ServerDockerImage"
TRootItem="ServerImage" Loader="Loader"
TCreateForm="CreateDockerImage" OnConfigure="OnConfigure"
TUpdateForm="UpdateDockerImage" OnConfigureCreate="OnConfigureForm"
RootItem="Image" OnConfigureEdit="OnConfigureForm">
Field="@(x => x.DockerImages)">
<View> <View>
<CrudColumn TItem="ServerDockerImage" Field="@(x => x.Id)" Title="Id"/> <MCBColumn TItem="ServerDockerImage" Field="@(x => x.Id)" Title="Id"/>
<CrudColumn TItem="ServerDockerImage" Field="@(x => x.DisplayName)" Title="Display name"/> <MCBColumn TItem="ServerDockerImage" Field="@(x => x.DisplayName)" Title="Display name"/>
<CrudColumn TItem="ServerDockerImage" Field="@(x => x.Name)" Title="Name"/> <MCBColumn TItem="ServerDockerImage" Field="@(x => x.Name)" Title="Name"/>
<CrudColumn TItem="ServerDockerImage" Field="@(x => x.AutoPull)" Title="Auto pull"> <MCBColumn TItem="ServerDockerImage" Field="@(x => x.AutoPull)" Title="Auto pull">
<Template> <Template>
@if (context.AutoPull) @if (context.AutoPull)
{ {
@@ -45,41 +25,78 @@
<i class="bx bx-sm bx-x text-danger"></i> <i class="bx bx-sm bx-x text-danger"></i>
} }
</Template> </Template>
</CrudColumn> </MCBColumn>
</View> </View>
<NoItemsView> </FastCrud>
<IconAlert Title="No docker images found" Color="primary" Icon="bx-search-alt">
Add a new docker image in order to get started. Need help? Check out our <a href="https://docs.moonlightpanel.xyz">documentation</a>
</IconAlert>
</NoItemsView>
</AutoListCrud>
@code @code
{ {
[Parameter] public ServerImage Image { get; set; } [Parameter] public ServerImage Image { get; set; }
[Parameter] public UpdateImageDetailedForm Form { get; set; }
private ServerDockerImage? SelectedDockerImage private IEnumerable<ServerDockerImage> Loader(Repository<ServerDockerImage> repository)
{ {
get return Image.DockerImages;
{
if (Form.DefaultDockerImage >= Image.DockerImages.Count)
return null;
if (Form.DefaultDockerImage == -1)
return null;
return Image.DockerImages[Form.DefaultDockerImage];
}
set
{
if (value == null)
{
Form.DefaultDockerImage = -1;
return;
} }
Form.DefaultDockerImage = Image.DockerImages.IndexOf(value); private void OnConfigure(FastCrudConfiguration<ServerDockerImage> configuration)
} {
configuration.CustomCreate = dockerImage =>
{
Image.DockerImages.Add(dockerImage);
ImageRepository.Update(Image);
return Task.CompletedTask;
};
configuration.CustomDelete = dockerImage =>
{
Image.DockerImages.Remove(dockerImage);
ImageRepository.Update(Image);
try
{
DockerImageRepository.Delete(dockerImage);
}
catch (Exception)
{
/* Dont fail here */
}
return Task.CompletedTask;
};
configuration.ValidateCreate = dockerImage =>
{
if (Image.DockerImages.Any(x => x.Name == dockerImage.Name))
throw new DisplayException("A docker image with this name does already exist");
return Task.CompletedTask;
};
configuration.ValidateEdit = dockerImage =>
{
if (Image.DockerImages.Any(x => x.Name == dockerImage.Name && x.Id != dockerImage.Id))
throw new DisplayException("A docker image with this name does already exist");
return Task.CompletedTask;
};
}
private void OnConfigureForm(FastFormConfiguration<ServerDockerImage> configuration, ServerDockerImage _)
{
configuration.AddProperty(x => x.Name)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithValidation(RegexValidator.Create("^(?:[a-zA-Z0-9\\-\\.]+\\/)?[a-zA-Z0-9\\-]+(?:\\/[a-zA-Z0-9\\-]+)*(?::[a-zA-Z0-9_\\.-]+)?$", "You need to provide a valid docker image name"))
.WithDescription("This is the name of the docker image. E.g. moonlightpanel/moonlight:canary");
configuration.AddProperty(x => x.DisplayName)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithDescription("This will be shown if the user is able to change the docker image as the image name");
configuration.AddProperty(x => x.AutoPull)
.WithComponent<SwitchComponent>()
.WithDescription("Specifies if the docker image should be pulled/updated when creating a server instance. Disable this for only local existing docker images");
} }
} }

View File

@@ -2,7 +2,7 @@
@using Moonlight.Features.FileManager.UI.Components @using Moonlight.Features.FileManager.UI.Components
<div class="mt-5 card card-body p-8"> <div class="mt-5 card card-body p-8">
<div class="row g-5"> <div class="row g-5">@*
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<SingleAutoProperty TItem="UpdateImageDetailedForm" <SingleAutoProperty TItem="UpdateImageDetailedForm"
Model="Form" Model="Form"
@@ -17,7 +17,7 @@
<SingleAutoProperty TItem="UpdateImageDetailedForm" <SingleAutoProperty TItem="UpdateImageDetailedForm"
Model="Form" Model="Form"
Field="@(x => x.AllocationsNeeded)"/> Field="@(x => x.AllocationsNeeded)"/>
</div> </div>*@
<div class="col-12"> <div class="col-12">
<Editor InitialContent="@Form.InstallScript" <Editor InitialContent="@Form.InstallScript"
Mode="sh" Mode="sh"

View File

@@ -0,0 +1,158 @@
@using Mappy.Net
@using Moonlight.Features.Servers.Models
@using Moonlight.Features.Servers.Models.Forms.Admin.Images.Parsing
@using Newtonsoft.Json
@inherits FastFormBaseComponent<string>
<div class="d-flex justify-content-end mb-3">
<button @onclick="AddConfig" type="button" class="btn btn-icon btn-success">
<i class="bx bx-sm bx-plus"></i>
</button>
</div>
@foreach (var config in Configs)
{
<div class="accordion mt-1" id="accordionPc">
<div class="accordion-item mb-3">
<div class="accordion-header" id="apch@(config.GetHashCode())">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#apcb@(config.GetHashCode())" aria-expanded="false" aria-controls="apcb@(config.GetHashCode())">
<span class="h5">@(string.IsNullOrEmpty(config.Key.File) ? "File path is missing" : config.Key.File)</span>
</button>
</div>
<div id="apcb@(config.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="apch@(config.GetHashCode())" data-bs-parent="#accordionPc">
<div class="accordion-body">
<div class="row g-5">
<div class="col-md-5">
<label class="form-label">File path</label>
<div class="form-text fs-5 mb-2 mt-0">
A relative path from the servers main directory to the file you want to modify
</div>
<input @bind="config.Key.File" @onfocusout="RefreshProperty" type="text" class="form-control" placeholder="e.g. configs/paper-global.yml">
</div>
<div class="col-md-5">
<label class="form-label">Type</label>
<div class="form-text fs-5 mb-2 mt-0">
This specifies the type of parser to use. e.g. "properties" or "file"
</div>
<input @bind="config.Key.Type" @onfocusout="RefreshProperty" type="text" class="form-control" placeholder="properties">
</div>
<div class="col-md-2">
<div class="text-end">
<WButton OnClick="() => RemoveConfig(config.Key)" CssClasses="btn btn-danger">Remove</WButton>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header border-bottom-0">
<div class="card-title"></div>
<div class="card-toolbar">
<button @onclick="() => AddOption(config.Key)" type="button" class="btn btn-icon btn-success">
<i class="bx bx-sm bx-plus"></i>
</button>
</div>
</div>
<div class="card-body pt-0">
<div class="row g-4">
@foreach (var option in config.Value)
{
<div class="col-md-6">
<div class="input-group">
<input @bind="option.Key" @onfocusout="RefreshProperty" type="text" class="form-control" placeholder="Key">
<input @bind="option.Value" @onfocusout="RefreshProperty" type="text" class="form-control" placeholder="Value (Variables with {{VARIABLE}})">
<WButton OnClick="() => RemoveOption(config.Key, option)" CssClasses="btn btn-danger">Remove</WButton>
</div>
</div>
}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
@code
{
[Parameter] public string InitialContent { get; set; }
private Dictionary<ParseConfigForm, List<ParseConfigOptionForm>> Configs = new();
protected override async Task OnInitializedAsync()
{
await Set(Binder.Value);
}
private async Task RefreshProperty()
{
Binder.Value = await Get();
}
public async Task Set(string content)
{
Configs.Clear();
var configs = JsonConvert.DeserializeObject<ServerParseConfig[]>(content) ?? [];
foreach (var config in configs)
{
var options = config.Configuration.Select(x => new ParseConfigOptionForm()
{
Key = x.Key,
Value = x.Value
}).ToList();
Configs.Add(Mapper.Map<ParseConfigForm>(config), options);
}
await InvokeAsync(StateHasChanged);
}
public Task<string> Get()
{
var finalConfigs = Configs.Select(x => new ServerParseConfig()
{
File = x.Key.File,
Type = x.Key.Type,
Configuration = x.Value.ToDictionary(y => y.Key, y => y.Value)
}).ToList();
var result = JsonConvert.SerializeObject(finalConfigs);
return Task.FromResult(result);
}
private async Task AddConfig()
{
Configs.Add(new(), new());
await InvokeAsync(StateHasChanged);
await RefreshProperty();
}
private async Task RemoveConfig(ParseConfigForm config)
{
Configs.Remove(config);
await InvokeAsync(StateHasChanged);
await RefreshProperty();
}
private async Task AddOption(ParseConfigForm config)
{
Configs[config].Add(new());
await InvokeAsync(StateHasChanged);
await RefreshProperty();
}
private async Task RemoveOption(ParseConfigForm config, ParseConfigOptionForm option)
{
Configs[config].Remove(option);
await InvokeAsync(StateHasChanged);
await RefreshProperty();
}
}

View File

@@ -1,7 +1,7 @@
@using Moonlight.Features.Servers.Models.Forms.Admin.Images @using Moonlight.Features.Servers.Models.Forms.Admin.Images
<div class="mt-5 card card-body p-8"> <div class="mt-5 card card-body p-8">
<div class="row g-5"> <div class="row g-5">@*
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<SingleAutoProperty TItem="UpdateImageDetailedForm" <SingleAutoProperty TItem="UpdateImageDetailedForm"
Model="Form" Model="Form"
@@ -16,7 +16,7 @@
<SingleAutoProperty TItem="UpdateImageDetailedForm" <SingleAutoProperty TItem="UpdateImageDetailedForm"
Model="Form" Model="Form"
Field="@(x => x.OnlineDetection)"/> Field="@(x => x.OnlineDetection)"/>
</div> </div>*@
</div> </div>
</div> </div>

View File

@@ -1,20 +1,23 @@
@using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Entities
@using Moonlight.Features.Servers.Models.Forms.Admin.Images.Variables @using MoonCore.Abstractions
@using BlazorTable @using MoonCore.Exceptions
@using Moonlight.Features.Servers.Entities.Enums
<AutoListCrud TItem="ServerImageVariable" @inject Repository<ServerImage> ImageRepository
TRootItem="ServerImage" @inject Repository<ServerImageVariable> VariableRepository
TCreateForm="CreateImageVariable"
TUpdateForm="UpdateImageVariable" <FastCrud TItem="ServerImageVariable"
RootItem="Image" Loader="Loader"
Field="@(x => x.Variables)"> OnConfigure="OnConfigure"
OnConfigureCreate="OnConfigureForm"
OnConfigureEdit="OnConfigureForm">
<View> <View>
<CrudColumn TItem="ServerImageVariable" Field="@(x => x.Id)" Title="Id"/> <MCBColumn TItem="ServerImageVariable" Field="@(x => x.Id)" Title="Id"/>
<CrudColumn TItem="ServerImageVariable" Field="@(x => x.DisplayName)" Title="Display name"/> <MCBColumn TItem="ServerImageVariable" Field="@(x => x.DisplayName)" Title="Display name"/>
<CrudColumn TItem="ServerImageVariable" Field="@(x => x.Key)" Title="Key"/> <MCBColumn TItem="ServerImageVariable" Field="@(x => x.Key)" Title="Key"/>
<CrudColumn TItem="ServerImageVariable" Field="@(x => x.DefaultValue)" Title="Default value"/> <MCBColumn TItem="ServerImageVariable" Field="@(x => x.DefaultValue)" Title="Default value"/>
<CrudColumn TItem="ServerImageVariable" Field="@(x => x.Filter)" Title="Filter"/> <MCBColumn TItem="ServerImageVariable" Field="@(x => x.Filter)" Title="Filter"/>
<CrudColumn TItem="ServerImageVariable" Field="@(x => x.AllowView)" Title="View"> <MCBColumn TItem="ServerImageVariable" Field="@(x => x.AllowView)" Title="View">
<Template> <Template>
@if (context.AllowView) @if (context.AllowView)
{ {
@@ -25,8 +28,8 @@
<i class="bx bx-sm bx-x text-danger"></i> <i class="bx bx-sm bx-x text-danger"></i>
} }
</Template> </Template>
</CrudColumn> </MCBColumn>
<CrudColumn TItem="ServerImageVariable" Field="@(x => x.AllowEdit)" Title="Edit"> <MCBColumn TItem="ServerImageVariable" Field="@(x => x.AllowEdit)" Title="Edit">
<Template> <Template>
@if (context.AllowEdit) @if (context.AllowEdit)
{ {
@@ -37,16 +40,98 @@
<i class="bx bx-sm bx-x text-danger"></i> <i class="bx bx-sm bx-x text-danger"></i>
} }
</Template> </Template>
</CrudColumn> </MCBColumn>
</View> </View>
<NoItemsView> </FastCrud>
<IconAlert Title="No variables found" Color="primary" Icon="bx-search-alt">
Add a new variable in order to get started. Need help? Check out our <a href="https://docs.moonlightpanel.xyz">documentation</a>
</IconAlert>
</NoItemsView>
</AutoListCrud>
@code @code
{ {
[Parameter] public ServerImage Image { get; set; } [Parameter] public ServerImage Image { get; set; }
private IEnumerable<ServerImageVariable> Loader(Repository<ServerImageVariable> _)
{
return Image.Variables;
}
private void OnConfigure(FastCrudConfiguration<ServerImageVariable> configuration)
{
configuration.CustomCreate = variable =>
{
Image.Variables.Add(variable);
ImageRepository.Update(Image);
return Task.CompletedTask;
};
configuration.CustomDelete = variable =>
{
Image.Variables.Remove(variable);
ImageRepository.Update(Image);
try
{
VariableRepository.Delete(variable);
}
catch (Exception)
{
/* dont fail here */
}
return Task.CompletedTask;
};
configuration.ValidateCreate = variable =>
{
if (Image.Variables.Any(x => x.Key == variable.Key))
throw new DisplayException("A variable with this key already exists");
return Task.CompletedTask;
};
configuration.ValidateEdit = variable =>
{
if (Image.Variables.Any(x => x.Key == variable.Key && x.Id != variable.Id))
throw new DisplayException("A variable with this key already exists");
return Task.CompletedTask;
};
}
private void OnConfigureForm(FastFormConfiguration<ServerImageVariable> configuration, ServerImageVariable _)
{
configuration.AddProperty(x => x.Key)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithDescription("This is the environment variable name");
configuration.AddProperty(x => x.DefaultValue)
.WithDefaultComponent()
.WithDescription("This is the default value which will be set when a server is created");
configuration.AddProperty(x => x.DisplayName)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithDescription("This is the display name of the variable which will be shown to the user if enabled to edit/view the variable");
configuration.AddProperty(x => x.Description)
.WithDefaultComponent()
.WithValidation(FastFormValidators.Required)
.WithDescription("This text should describe what the variable does for the user if allowed to view and/or change");
configuration.AddProperty(x => x.AllowView)
.WithComponent<SwitchComponent>()
.WithDescription("Allow the user to view the variable but not edit it unless specified otherwise");
configuration.AddProperty(x => x.AllowEdit)
.WithComponent<SwitchComponent>()
.WithDescription("Allow the user to edit the variable. Wont work if view is disabled");
configuration.AddProperty(x => x.Type)
.WithComponent<EnumSelectComponent<ServerImageVariableType>>()
.WithDescription("Specifies the type of the variable. This specifies what ui the user will see for the variable. You can also specify the options which are available using the filter field");
configuration.AddProperty(x => x.Filter)
.WithDefaultComponent()
.WithDescription("(Optional)\nText: A regex filter which will check if the user input mathes a correct variable value\nSelect: Specify the available values seperated by a semicolon");
}
} }

View File

@@ -3,8 +3,6 @@
@using Moonlight.Features.Servers.Services @using Moonlight.Features.Servers.Services
@using Moonlight.Features.Servers.UI.Components @using Moonlight.Features.Servers.UI.Components
@using MoonCore.Abstractions @using MoonCore.Abstractions
@using MoonCore.Helpers
@using MoonCoreUI.Services
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Moonlight.Features.Servers.Helpers @using Moonlight.Features.Servers.Helpers
@using Moonlight.Features.Servers.UI.UserViews @using Moonlight.Features.Servers.UI.UserViews
@@ -12,8 +10,7 @@
@using System.Net.WebSockets @using System.Net.WebSockets
@using MoonCore.Exceptions @using MoonCore.Exceptions
@using Moonlight.Features.Servers.Configuration @using Moonlight.Features.Servers.Configuration
@using MoonCore.Services @using MoonCore.Blazor.Forms.Router
@using Moonlight.Core.Services
@inject Repository<Server> ServerRepository @inject Repository<Server> ServerRepository
@inject ServerService ServerService @inject ServerService ServerService
@@ -21,10 +18,12 @@
@inject AlertService AlertService @inject AlertService AlertService
@inject IdentityService IdentityService @inject IdentityService IdentityService
@inject ConfigService<ServersConfiguration> ConfigService @inject ConfigService<ServersConfiguration> ConfigService
@inject ILogger<UserLayout> Logger
@inject ILoggerFactory LoggerFactory
@implements IDisposable @implements IDisposable
<LazyLoader Load="Load" ShowAsCard="true"> <LazyLoader Load="Load">
<div class="card card-body pb-5 pt-5"> <div class="card card-body pb-5 pt-5">
<div> <div>
<div class="row px-2"> <div class="row px-2">
@@ -154,7 +153,7 @@
{ {
<CascadingValue Value="Server"> <CascadingValue Value="Server">
<CascadingValue Value="Console"> <CascadingValue Value="Console">
<SmartRouter Route="@Route"> <MCBRouter Route="@Route">
<Route Path="/"> <Route Path="/">
<Console/> <Console/>
</Route> </Route>
@@ -186,7 +185,7 @@
<Route Path="/reset"> <Route Path="/reset">
<Reset/> <Reset/>
</Route> </Route>
</SmartRouter> </MCBRouter>
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>
} }
@@ -224,7 +223,7 @@
.Include(x => x.Owner) .Include(x => x.Owner)
.First(x => x.Id == Id); .First(x => x.Id == Id);
if (Server.Owner.Id != IdentityService.CurrentUser.Id && IdentityService.CurrentUser.Permissions < 5000) if (Server.Owner.Id != IdentityService.GetUser().Id && IdentityService.GetUser().Permissions < 5000)
{ {
Server = null!; Server = null!;
return; return;
@@ -233,7 +232,7 @@
await lazyLoader.SetText("Establishing a connection to the server"); await lazyLoader.SetText("Establishing a connection to the server");
// Create console wrapper // Create console wrapper
Console = new ServerConsole(Server); Console = new ServerConsole(Server, LoggerFactory);
// Configure // Configure
Console.OnStateChange += async state => await HandleStateChange(state); Console.OnStateChange += async state => await HandleStateChange(state);
@@ -266,7 +265,7 @@
if (httpRequestException.InnerException is not SocketException socketException) if (httpRequestException.InnerException is not SocketException socketException)
throw; throw;
Logger.Warn($"Unable to access the node's websocket endpoint: {socketException.Message}"); Logger.LogWarning("Unable to access the node's websocket endpoint: {socketException}", socketException);
// Change the ui and... // Change the ui and...
IsNodeOffline = true; IsNodeOffline = true;
@@ -328,7 +327,12 @@
{ {
if (!ConfigService.Get().DisableServerKillWarning) if (!ConfigService.Get().DisableServerKillWarning)
{ {
if (!await AlertService.YesNo("Do you really want to kill the server? This can result in data loss or corrupted server files")) await AlertService.Confirm(
"Server kill confirmation",
"Do you really want to kill the server? This can result in data loss or corrupted server files",
async () => await SendSignalHandled(PowerAction.Kill)
);
return; return;
} }
@@ -347,8 +351,7 @@
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn($"An error occured while sending power action {action} to server {Server.Id}:"); Logger.LogWarning("An error occured while sending power action {action} to server {serverId}: {e}", action, Server.Id, e);
Logger.Warn(e);
await ToastService.Danger("An error occured while sending power action to server. Check the console for more information"); await ToastService.Danger("An error occured while sending power action to server. Check the console for more information");
} }

Some files were not shown because too many files have changed in this diff Show More