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>
</SmartForm> </div>
</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;
await IdentityService.SetFlag("CookieAsked", true);
await IdentityService.SetFlag("CookieConsent", true);
await InvokeAsync(StateHasChanged);
}
private async Task Decline()
{
if (!IdentityService.IsLoggedIn)
return; return;
await IdentityService.SetFlag("CookieAsked", true); await IdentityService.SetFlag("CookieAsked", true);
if (answer)
await IdentityService.SetFlag("CookieConsent", true);
await InvokeAsync(StateHasChanged); 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,63 +17,83 @@
<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="col-md-3 col-12">
<div class="row g-5"> <div class="card card-body h-100">
<div class="col-md-3 col-12"> <div class="d-flex flex-column justify-content-center">
<div class="card card-body h-100"> <div class="d-flex justify-content-center">
<div class="d-flex flex-column justify-content-center"> <div class="symbol symbol-100px symbol-lg-160px symbol-fixed">
<div class="d-flex justify-content-center"> <img src="/api/core/avatar" alt="image">
<div class="symbol symbol-100px symbol-lg-160px symbol-fixed">
<img src="/api/core/avatar" alt="image">
</div>
</div>
<div class="fs-4 mt-5 text-center">
To change your profile picture go to <a href="https://gravatar.com/">Gravatar</a> and
register with the same email address you are using here
</div> </div>
</div> </div>
<div class="fs-4 mt-5 text-center">
To change your profile picture go to <a href="https://gravatar.com/">Gravatar</a> and
register with the same email address you are using here
</div>
</div> </div>
</div> </div>
<div class="col-md-9 col-12"> </div>
<div class="card"> <div class="col-md-9 col-12">
<SmartForm Model="Form" OnValidSubmit="Update"> <div class="card">
<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>
</SmartForm>
</div> </div>
</div> </div>
</div> </div>
</LazyLoader> </div>
@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,81 +17,104 @@
<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"> <div class="card-body">
<SmartForm Model="PasswordForm" OnValidSubmit="OnValidSubmitPassword"> <FastForm @ref="PasswordForm" Model="PasswordModel" OnConfigure="OnConfigurePasswordForm" />
<div class="card-body">
<div class="row">
<AutoForm Model="PasswordForm" Columns="12" />
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<button class="btn btn-primary" type="submit">Save changes</button>
</div>
</SmartForm>
</div> </div>
</div> <div class="card-footer d-flex justify-content-end">
<div class="col-md-6 col-12"> <WButton OnClick="ChangePassword" CssClasses="btn btn-primary">Save changes</WButton>
<div class="card h-100">
<div class="card-body">
<TwoFactorWizard />
</div>
</div>
</div>
<div class="col-md-6 col-12">
<div class="card h-100">
<SmartForm Model="CookiesForm" OnValidSubmit="OnValidSubmitCookie">
<div class="card-body">
<h3>Cookies</h3>
<div>
<AutoForm Model="CookiesForm"/>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<button class="btn btn-primary" type="submit">Save changes</button>
</div>
</SmartForm>
</div> </div>
</div> </div>
</div> </div>
</LazyLoader> <div class="col-md-6 col-12">
<div class="card h-100">
<div class="card-body">
<TwoFactorWizard />
</div>
</div>
</div>
<div class="col-md-6 col-12">
<div class="card h-100">
<div class="card-body">
<h3>Cookies</h3>
<p class="my-2 text-muted fs-5">
This specifies if you would like to personalize your experience with optional cookies.
</p>
<div class="form-check form-switch">
@if (CookieConsent)
{
<input @onclick="() => SetCookieConsent(false)" class="form-check-input" type="checkbox" role="switch" checked="checked">
}
else
{
<input @onclick="() => SetCookieConsent(true)" class="form-check-input" type="checkbox" role="switch">
}
</div>
</div>
</div>
</div>
</div>
@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 = "[]";
var key = Formatter.GenerateString(32);
apiKey.Key = key;
await ClipboardService.Copy(key);
await ToastService.Info("Copied api key into your clipboard");
};
} }
private async Task ValidateAdd(ApiKey apiKey) private void OnConfigureFrom(FastFormConfiguration<ApiKey> configuration, ApiKey _)
{ {
var key = Formatter.GenerateString(32); configuration.AddProperty(x => x.Description)
apiKey.Key = key; .WithDefaultComponent()
.WithDescription("Write a note here for which application the api key is used for")
.WithValidation(FastFormValidators.Required);
await ClipboardService.Copy(key); configuration.AddProperty(x => x.ExpiresAt)
await ToastService.Info("Copied api key into your clipboard"); .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
@@ -12,9 +12,9 @@
@attribute [RequirePermission(9999)] @attribute [RequirePermission(9999)]
<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"> </LazyLoader>
@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>
</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,15 +149,37 @@ 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
if (string.IsNullOrEmpty(newPassword))
return;
// This handles empty and canceled input await AuthenticationProvider.ChangePassword(user, newPassword);
if (string.IsNullOrEmpty(newPassword)) });
return;
await AuthenticationProvider.ChangePassword(user, newPassword);
} }
private async Task Add(User user) private void OnConfigure(FastCrudConfiguration<User> configuration)
{ {
var result = await AuthenticationProvider.Register(user.Username, user.Email, user.Password); configuration.CustomCreate = async user =>
{
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");
};
configuration.ValidateEdit = async user =>
{
await AuthenticationProvider.ChangeDetails(user, user.Email, user.Username);
};
} }
// To notify the authentication provider before we update the data in the database, we call it here private void OnConfigureCreate(FastFormConfiguration<User> configuration, User _)
private async Task ValidateUpdate(User user)
{ {
await AuthenticationProvider.ChangeDetails(user, user.Email, user.Username); 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,40 +94,42 @@
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))
return;
try
{ {
session.NavigationManager.NavigateTo(url); if (string.IsNullOrEmpty(url))
return;
await ToastService.Success("Successfully redirected user session"); try
} {
catch (Exception) session.NavigationManager.NavigateTo(url);
{
await ToastService.Danger("Unable to redirect user. The user is probably no longer connect with moonlight"); await ToastService.Success("Successfully redirected user session");
} }
catch (Exception)
{
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))
return;
try
{ {
await session.AlertService.Info(message); if (string.IsNullOrEmpty(message))
return;
await ToastService.Success("Successfully sent message to user session"); try
} {
catch (Exception) await session.AlertService.Info(message);
{
await ToastService.Danger("Unable to send message. The user is probably no longer connect with moonlight"); await ToastService.Success("Successfully sent message to user session");
} }
catch (Exception)
{
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,37 +23,39 @@ 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",
async fileName =>
{
if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled
return;
var toastService = provider.GetRequiredService<ToastService>();
await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient");
try
{
await archiveAccess.Archive(
access.CurrentDirectory + fileName,
new[] { access.CurrentDirectory + entry.Name }
);
await toastService.Success("Successfully created archive");
}
catch (Exception e)
{
var logger = provider.GetRequiredService<ILogger<ArchiveContextAction>>();
logger.LogWarning("An error occured while archiving item ({name}): {e}", entry.Name, e);
await toastService.Danger("An unknown error occured while creating archive");
}
finally
{
await toastService.DeleteProgress("fileManagerArchive");
}
await fileManager.View.Refresh();
},
Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz"); Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz");
if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled
return;
var toastService = provider.GetRequiredService<ToastService>();
await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient");
try
{
await archiveAccess.Archive(
access.CurrentDirectory + fileName,
new[] { access.CurrentDirectory + entry.Name }
);
await toastService.Success("Successfully created archive");
}
catch (Exception e)
{
Logger.Warn($"An error occured while archiving item ({entry.Name}):");
Logger.Warn(e);
await toastService.Danger("An unknown error occured while creating archive");
}
finally
{
await toastService.RemoveProgress("fileManagerArchive");
}
await fileManager.View.Refresh();
} }
} }

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,35 +21,36 @@ 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 =>
{
if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled
return;
var toastService = provider.GetRequiredService<ToastService>();
await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient");
try
{
await archiveAccess.Archive(
access.CurrentDirectory + fileName,
entries.Select(x => access.CurrentDirectory + x.Name).ToArray()
);
await toastService.Success("Successfully created archive");
}
catch (Exception e)
{
var logger = provider.GetRequiredService<ILogger<ArchiveSelectionAction>>();
logger.LogWarning("An error occured while archiving items ({lenght}): {e}", entries.Length, e);
await toastService.Danger("An unknown error occured while creating archive");
}
finally
{
await toastService.DeleteProgress("fileManagerArchive");
}
},
Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz"); Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz");
if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled
return;
var toastService = provider.GetRequiredService<ToastService>();
await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient");
try
{
await archiveAccess.Archive(
access.CurrentDirectory + fileName,
entries.Select(x => access.CurrentDirectory + x.Name).ToArray()
);
await toastService.Success("Successfully created archive");
}
catch (Exception e)
{
Logger.Warn($"An error occured while archiving items ({entries.Length}):");
Logger.Warn(e);
await toastService.Danger("An unknown error occured while creating archive");
}
finally
{
await toastService.RemoveProgress("fileManagerArchive");
}
} }
} }

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,21 +14,22 @@ 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(".."))
return;
await access.CreateFile(name);
// We build a virtual entry here so we dont need to fetch one
await fileManager.OpenEditor(new()
{ {
Name = name, if (string.IsNullOrEmpty(name) || name.Contains(".."))
Size = 0, return;
IsFile = true,
IsDirectory = false, await access.CreateFile(name);
LastModifiedAt = DateTime.UtcNow
// We build a virtual entry here so we dont need to fetch one
await fileManager.OpenEditor(new()
{
Name = name,
Size = 0,
IsFile = true,
IsDirectory = false,
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,14 +16,15 @@ 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(".."))
return;
if (string.IsNullOrEmpty(name) || name.Contains("..")) await access.CreateDirectory(name);
return;
await access.CreateDirectory(name); await toastService.Success("Successfully created directory");
await fileManager.View.Refresh();
await toastService.Success("Successfully created directory"); });
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)
{
await toastService.UpdateProgress("fileManagerSelectionDelete", $"Deleting '{entry.Name}'");
foreach (var entry in entries) await access.Delete(entry);
{ }
await toastService.ModifyProgress("fileManagerSelectionDelete", $"Deleting '{entry.Name}'");
await access.Delete(entry); await toastService.DeleteProgress("fileManagerSelectionDelete");
}
await toastService.RemoveProgress("fileManagerSelectionDelete"); await toastService.Success("Successfully deleted selection");
await fileManager.View.Refresh();
await toastService.Success("Successfully deleted selection"); });
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,14 +18,15 @@ 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))
return;
if (string.IsNullOrEmpty(newName)) await access.Rename(entry.Name, newName);
return;
await access.Rename(entry.Name, newName); await fileManager.View.Refresh();
await toastService.Success("Successfully renamed file");
await fileManager.View.Refresh(); }, entry.Name);
await toastService.Success("Successfully renamed file");
} }
} }

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(); {
parameters.Add("Title", title);
FolderSelectResult = onResult; parameters.Add("OnResult", onResult);
FolderSelectTitle = title; parameters.Add("FileAccess", FileAccess.Clone());
});
FolderSelectFileAccess = FileAccess.Clone();
await FolderSelectFileAccess.SetDirectory("/");
await FolderSelectModal.Show();
} }
public async Task HideFolderSelect()
{
await FolderSelectModal.Hide();
FolderSelectIsOpen = false;
FolderSelectFileAccess.Dispose();
}
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,85 +1,102 @@
@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)
{ {
<i class="bx bx-sm bx-check text-success"></i> <i class="bx bx-sm bx-check text-success"></i>
} }
else else
{ {
<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) private void OnConfigure(FastCrudConfiguration<ServerDockerImage> configuration)
return null; {
configuration.CustomCreate = dockerImage =>
return Image.DockerImages[Form.DefaultDockerImage];
}
set
{ {
if (value == null) Image.DockerImages.Add(dockerImage);
ImageRepository.Update(Image);
return Task.CompletedTask;
};
configuration.CustomDelete = dockerImage =>
{
Image.DockerImages.Remove(dockerImage);
ImageRepository.Update(Image);
try
{ {
Form.DefaultDockerImage = -1; DockerImageRepository.Delete(dockerImage);
return; }
catch (Exception)
{
/* Dont fail here */
} }
Form.DefaultDockerImage = Image.DockerImages.IndexOf(value); 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,52 +1,137 @@
@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)
{ {
<i class="bx bx-sm bx-check text-success"></i> <i class="bx bx-sm bx-check text-success"></i>
} }
else else
{ {
<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)
{ {
<i class="bx bx-sm bx-check text-success"></i> <i class="bx bx-sm bx-check text-success"></i>
} }
else else
{ {
<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">
@@ -43,21 +42,21 @@
<div class="col"> <div class="col">
<div class="text-gray-900 fs-4"> <div class="text-gray-900 fs-4">
@{ @{
var color = ServerUtilsHelper.GetColorFromState(Console.State); var color = ServerUtilsHelper.GetColorFromState(Console.State);
} }
<i class="bx bx-sm bxs-circle text-@(color) @(Console.State != ServerState.Offline ? $"pulse pulse-{color}" : "") align-middle"></i> <i class="bx bx-sm bxs-circle text-@(color) @(Console.State != ServerState.Offline ? $"pulse pulse-{color}" : "") align-middle"></i>
<span class="align-middle"> <span class="align-middle">
@(Console.State) @(Console.State)
<span class="text-muted">(@(Formatter.FormatUptime(DateTime.UtcNow - Console.LastStateChangeTimestamp)))</span> <span class="text-muted">(@(Formatter.FormatUptime(DateTime.UtcNow - Console.LastStateChangeTimestamp)))</span>
</span> </span>
</div> </div>
<div class="text-gray-800 pt-3 fw-semibold fs-5 row"> <div class="text-gray-800 pt-3 fw-semibold fs-5 row">
<div class="col-auto"> <div class="col-auto">
<span> <span>
<i class="bx bx-sm bx-globe align-middle text-info"></i> <i class="bx bx-sm bx-globe align-middle text-info"></i>
<span class="align-middle">@(Server.Node.Fqdn):@(Server.MainAllocation.Port)</span> <span class="align-middle">@(Server.Node.Fqdn):@(Server.MainAllocation.Port)</span>
</span> </span>
</div> </div>
@* @*
<div class="col-auto"> <div class="col-auto">
@@ -73,47 +72,47 @@
<div class="mt-2"> <div class="mt-2">
@if (Console.State == ServerState.Offline) @if (Console.State == ServerState.Offline)
{ {
<WButton <WButton
OnClick="Start" OnClick="Start"
CssClasses="btn btn-light-success btn-icon me-1 my-1"> CssClasses="btn btn-light-success btn-icon me-1 my-1">
<i class="bx bx-sm bx-play"></i> <i class="bx bx-sm bx-play"></i>
</WButton> </WButton>
} }
else else
{ {
<button type="button" class="btn btn-light-success btn-icon me-1 my-1 disabled" disabled=""> <button type="button" class="btn btn-light-success btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-play"></i> <i class="bx bx-sm bx-play"></i>
</button> </button>
} }
@if (Console.State == ServerState.Offline || Console.State == ServerState.Installing) @if (Console.State == ServerState.Offline || Console.State == ServerState.Installing)
{ {
<button class="btn btn-light-warning btn-icon me-1 my-1 disabled" disabled=""> <button class="btn btn-light-warning btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-power-off"></i> <i class="bx bx-sm bx-power-off"></i>
</button> </button>
} }
else else
{ {
<WButton <WButton
OnClick="Stop" OnClick="Stop"
CssClasses="btn btn-light-warning btn-icon me-1 my-1"> CssClasses="btn btn-light-warning btn-icon me-1 my-1">
<i class="bx bx-sm bx-power-off"></i> <i class="bx bx-sm bx-power-off"></i>
</WButton> </WButton>
} }
@if (Console.State == ServerState.Offline || Console.State == ServerState.Installing) @if (Console.State == ServerState.Offline || Console.State == ServerState.Installing)
{ {
<button class="btn btn-light-danger btn-icon me-1 my-1 disabled" disabled=""> <button class="btn btn-light-danger btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-bomb"></i> <i class="bx bx-sm bx-bomb"></i>
</button> </button>
} }
else else
{ {
<WButton <WButton
OnClick="Kill" OnClick="Kill"
CssClasses="btn btn-light-danger btn-icon me-1 my-1"> CssClasses="btn btn-light-danger btn-icon me-1 my-1">
<i class="bx bx-sm bx-bomb"></i> <i class="bx bx-sm bx-bomb"></i>
</WButton> </WButton>
} }
</div> </div>
</div> </div>
@@ -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,8 +327,13 @@
{ {
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(
return; "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;
} }
await SendSignalHandled(PowerAction.Kill); await SendSignalHandled(PowerAction.Kill);
@@ -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