From d024a834f96ea2dfae33a227e2c977d4bfb34649 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Thu, 6 Jul 2023 16:46:01 +0200 Subject: [PATCH] Added a new visual config editor --- Moonlight/App/Configuration/ConfigV1.cs | 174 ++++++++++++++---- Moonlight/App/Helpers/BlurAttribute.cs | 6 + Moonlight/App/Helpers/Formatter.cs | 29 ++- Moonlight/App/Helpers/PropBinder.cs | 51 +++++ Moonlight/App/Services/ConfigService.cs | 22 ++- .../Components/Forms/SmartFormClass.razor | 65 +++++++ .../Components/Forms/SmartFormProperty.razor | 79 ++++++++ .../Navigations/AdminSystemNavigation.razor | 5 + .../Views/Admin/Sys/Configuration.razor | 53 ++++++ 9 files changed, 442 insertions(+), 42 deletions(-) create mode 100644 Moonlight/App/Helpers/BlurAttribute.cs create mode 100644 Moonlight/App/Helpers/PropBinder.cs create mode 100644 Moonlight/Shared/Components/Forms/SmartFormClass.razor create mode 100644 Moonlight/Shared/Components/Forms/SmartFormProperty.razor create mode 100644 Moonlight/Shared/Views/Admin/Sys/Configuration.razor diff --git a/Moonlight/App/Configuration/ConfigV1.cs b/Moonlight/App/Configuration/ConfigV1.cs index 67224fd..053aa1c 100644 --- a/Moonlight/App/Configuration/ConfigV1.cs +++ b/Moonlight/App/Configuration/ConfigV1.cs @@ -1,19 +1,25 @@ -namespace Moonlight.App.Configuration; +using System.ComponentModel; +using Moonlight.App.Helpers; + +namespace Moonlight.App.Configuration; using System; using Newtonsoft.Json; public class ConfigV1 { - [JsonProperty("Moonlight")] public MoonlightData Moonlight { get; set; } = new(); + [JsonProperty("Moonlight")] + public MoonlightData Moonlight { get; set; } = new(); public class MoonlightData { - [JsonProperty("AppUrl")] public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash"; + [JsonProperty("AppUrl")] + [Description("The url moonlight is accesible with from the internet")] + public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash"; [JsonProperty("Database")] public DatabaseData Database { get; set; } = new(); - [JsonProperty("DiscordBotApi")] public DiscordBotData DiscordBotApi { get; set; } = new(); + [JsonProperty("DiscordBotApi")] public DiscordBotApiData DiscordBotApi { get; set; } = new(); [JsonProperty("DiscordBot")] public DiscordBotData DiscordBot { get; set; } = new(); @@ -47,17 +53,29 @@ public class ConfigV1 public class CleanupData { - [JsonProperty("Cpu")] public long Cpu { get; set; } = 90; + [JsonProperty("Cpu")] + [Description("The maximum amount of cpu usage in percent a node is allowed to use before the cleanup starts")] + public long Cpu { get; set; } = 90; - [JsonProperty("Memory")] public long Memory { get; set; } = 8192; + [JsonProperty("Memory")] + [Description("The minumum amount of memory in megabytes avaliable before the cleanup starts")] + public long Memory { get; set; } = 8192; - [JsonProperty("Wait")] public long Wait { get; set; } = 15; + [JsonProperty("Wait")] + [Description("The delay between every cleanup check in minutes")] + public long Wait { get; set; } = 15; - [JsonProperty("Uptime")] public long Uptime { get; set; } = 6; + [JsonProperty("Uptime")] + [Description("The maximum uptime of any server in hours before it the server restarted by the cleanup system")] + public long Uptime { get; set; } = 6; - [JsonProperty("Enable")] public bool Enable { get; set; } = false; + [JsonProperty("Enable")] + [Description("The cleanup system provides a fair way for stopping unused servers and staying stable even with overallocation. A detailed explanation: docs.endelon-hosting.de/erklaerungen/cleanup")] + public bool Enable { get; set; } = false; - [JsonProperty("MinUptime")] public long MinUptime { get; set; } = 10; + [JsonProperty("MinUptime")] + [Description("The minumum uptime of a server in minutes to prevent stopping servers which just started")] + public long MinUptime { get; set; } = 10; } public class DatabaseData @@ -65,39 +83,77 @@ public class ConfigV1 [JsonProperty("Database")] public string Database { get; set; } = "moonlight_db"; [JsonProperty("Host")] public string Host { get; set; } = "your.database.host"; - - [JsonProperty("Password")] public string Password { get; set; } = "secret"; + + [JsonProperty("Password")] + [Blur] + public string Password { get; set; } = "secret"; [JsonProperty("Port")] public long Port { get; set; } = 3306; [JsonProperty("Username")] public string Username { get; set; } = "moonlight_user"; } + public class DiscordBotApiData + { + [JsonProperty("Enable")] + [Description("Enable the discord bot api. Currently only DatBot is using this api")] + public bool Enable { get; set; } = false; + + [JsonProperty("Token")] + [Description("Specify the token the api client needs to provide")] + [Blur] + public string Token { get; set; } = Guid.NewGuid().ToString(); + } public class DiscordBotData { - [JsonProperty("Enable")] public bool Enable { get; set; } = false; + [JsonProperty("Enable")] + [Description("The discord bot can be used to allow customers to manage their servers via discord")] + public bool Enable { get; set; } = false; - [JsonProperty("Token")] public string Token { get; set; } = "discord token here"; + [JsonProperty("Token")] + [Description("Your discord bot token goes here")] + [Blur] + public string Token { get; set; } = "discord token here"; - [JsonProperty("PowerActions")] public bool PowerActions { get; set; } = false; - [JsonProperty("SendCommands")] public bool SendCommands { get; set; } = false; + [JsonProperty("PowerActions")] + [Description("Enable actions like starting and stopping servers")] + public bool PowerActions { get; set; } = false; + + [JsonProperty("SendCommands")] + [Description("Allow users to send commands to their servers")] + public bool SendCommands { get; set; } = false; } public class DiscordNotificationsData { - [JsonProperty("Enable")] public bool Enable { get; set; } = false; + [JsonProperty("Enable")] + [Description("The discord notification system sends you a message everytime a event like a new support chat message is triggered with usefull data describing the event")] + public bool Enable { get; set; } = false; - [JsonProperty("WebHook")] public string WebHook { get; set; } = "http://your-discord-webhook-url"; + [JsonProperty("WebHook")] + [Description("The discord webhook the notifications are being sent to")] + [Blur] + public string WebHook { get; set; } = "http://your-discord-webhook-url"; } public class DomainsData { - [JsonProperty("Enable")] public bool Enable { get; set; } = false; - [JsonProperty("AccountId")] public string AccountId { get; set; } = "cloudflare acc id"; + [JsonProperty("Enable")] + [Description("This enables the domain system")] + public bool Enable { get; set; } = false; + + [JsonProperty("AccountId")] + [Description("This option specifies the cloudflare account id")] + public string AccountId { get; set; } = "cloudflare acc id"; - [JsonProperty("Email")] public string Email { get; set; } = "cloudflare@acc.email"; + [JsonProperty("Email")] + [Description("This specifies the cloudflare email to use for communicating with the cloudflare api")] + public string Email { get; set; } = "cloudflare@acc.email"; - [JsonProperty("Key")] public string Key { get; set; } = "secret"; + [JsonProperty("Key")] + [Description("Your cloudflare api key goes here")] + [Blur] + public string Key { get; set; } = "secret"; } public class HtmlData @@ -107,13 +163,21 @@ public class ConfigV1 public class HeadersData { - [JsonProperty("Color")] public string Color { get; set; } = "#4b27e8"; + [JsonProperty("Color")] + [Description("This specifies the color of the embed generated by platforms like discord when someone posts a link to your moonlight instance")] + public string Color { get; set; } = "#4b27e8"; - [JsonProperty("Description")] public string Description { get; set; } = "the next generation hosting panel"; + [JsonProperty("Description")] + [Description("This specifies the description text of the embed generated by platforms like discord when someone posts a link to your moonlight instance and can also help google to index your moonlight instance correctly")] + public string Description { get; set; } = "the next generation hosting panel"; - [JsonProperty("Keywords")] public string Keywords { get; set; } = "moonlight"; + [JsonProperty("Keywords")] + [Description("To help search engines like google to index your moonlight instance correctly you can specify keywords seperated by a comma here")] + public string Keywords { get; set; } = "moonlight"; - [JsonProperty("Title")] public string Title { get; set; } = "Moonlight - endelon.link"; + [JsonProperty("Title")] + [Description("This specifies the title of the embed generated by platforms like discord when someone posts a link to your moonlight instance")] + public string Title { get; set; } = "Moonlight - endelon.link"; } public class MailData @@ -122,7 +186,9 @@ public class ConfigV1 [JsonProperty("Server")] public string Server { get; set; } = "your.mail.host"; - [JsonProperty("Password")] public string Password { get; set; } = "secret"; + [JsonProperty("Password")] + [Blur] + public string Password { get; set; } = "secret"; [JsonProperty("Port")] public int Port { get; set; } = 465; @@ -142,9 +208,13 @@ public class ConfigV1 public class OAuth2Data { - [JsonProperty("OverrideUrl")] public string OverrideUrl { get; set; } = "https://only-for-development.cases"; + [JsonProperty("OverrideUrl")] + [Description("This overrides the redirect url which would be typicaly the app url")] + public string OverrideUrl { get; set; } = "https://only-for-development.cases"; - [JsonProperty("EnableOverrideUrl")] public bool EnableOverrideUrl { get; set; } = false; + [JsonProperty("EnableOverrideUrl")] + [Description("This enables the url override")] + public bool EnableOverrideUrl { get; set; } = false; [JsonProperty("Providers")] public OAuth2ProviderData[] Providers { get; set; } = Array.Empty(); @@ -156,41 +226,65 @@ public class ConfigV1 [JsonProperty("ClientId")] public string ClientId { get; set; } - [JsonProperty("ClientSecret")] public string ClientSecret { get; set; } + [JsonProperty("ClientSecret")] + [Blur] + public string ClientSecret { get; set; } } public class RatingData { - [JsonProperty("Enabled")] public bool Enabled { get; set; } = false; + [JsonProperty("Enabled")] + [Description("The rating systems shows a user who is registered longer than the set amout of days a popup to rate this platform if he hasnt rated it before")] + public bool Enabled { get; set; } = false; - [JsonProperty("Url")] public string Url { get; set; } = "https://link-to-google-or-smth"; + [JsonProperty("Url")] + [Description("This is the url a user who rated above a set limit is shown to rate you again. Its recommended to put your google or trustpilot rate link here")] + public string Url { get; set; } = "https://link-to-google-or-smth"; - [JsonProperty("MinRating")] public int MinRating { get; set; } = 4; + [JsonProperty("MinRating")] + [Description("The minimum star count on the rating ranging from 1 to 5")] + public int MinRating { get; set; } = 4; - [JsonProperty("DaysSince")] public int DaysSince { get; set; } = 5; + [JsonProperty("DaysSince")] + [Description("The days a user has to be registered to even be able to get this popup")] + public int DaysSince { get; set; } = 5; } public class SecurityData { - [JsonProperty("Token")] public string Token { get; set; } = Guid.NewGuid().ToString(); + [JsonProperty("Token")] + [Description("This is the moonlight app token. It is used to encrypt and decrypt data and validte tokens and sessions")] + [Blur] + public string Token { get; set; } = Guid.NewGuid().ToString(); [JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new(); } public class ReCaptchaData { - [JsonProperty("Enable")] public bool Enable { get; set; } = false; + [JsonProperty("Enable")] + [Description("Enables repatcha at places like the register page. For information how to get your recaptcha credentails go to google.com/recaptcha/about/")] + public bool Enable { get; set; } = false; - [JsonProperty("SiteKey")] public string SiteKey { get; set; } = "recaptcha site key here"; + [JsonProperty("SiteKey")] + [Blur] + public string SiteKey { get; set; } = "recaptcha site key here"; - [JsonProperty("SecretKey")] public string SecretKey { get; set; } = "recaptcha secret here"; + [JsonProperty("SecretKey")] + [Blur] + public string SecretKey { get; set; } = "recaptcha secret here"; } public class SentryData { - [JsonProperty("Enable")] public bool Enable { get; set; } = false; + [JsonProperty("Enable")] + [Description("Sentry is a way to monitor application crashes and performance issues in real time. Enable this option only if you set a sentry dsn")] + public bool Enable { get; set; } = false; - [JsonProperty("Dsn")] public string Dsn { get; set; } = "http://your-sentry-url-here"; + [JsonProperty("Dsn")] + [Description("The dsn is the key moonlight needs to communicate with your sentry instance")] + [Blur] + public string Dsn { get; set; } = "http://your-sentry-url-here"; } public class SmartDeployData diff --git a/Moonlight/App/Helpers/BlurAttribute.cs b/Moonlight/App/Helpers/BlurAttribute.cs new file mode 100644 index 0000000..170c2b4 --- /dev/null +++ b/Moonlight/App/Helpers/BlurAttribute.cs @@ -0,0 +1,6 @@ +namespace Moonlight.App.Helpers; + +public class BlurAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index 31834cc..44d08f5 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -1,9 +1,36 @@ -using Moonlight.App.Services; +using System.Text; +using Moonlight.App.Services; namespace Moonlight.App.Helpers; public static class Formatter { + public static string ReplaceEnd(string input, string substringToReplace, string newSubstring) + { + int lastIndexOfSubstring = input.LastIndexOf(substringToReplace); + if (lastIndexOfSubstring >= 0) + { + input = input.Remove(lastIndexOfSubstring, substringToReplace.Length).Insert(lastIndexOfSubstring, newSubstring); + } + + return input; + } + public static string ConvertCamelCaseToSpaces(string input) + { + StringBuilder output = new StringBuilder(); + + foreach (char c in input) + { + if (char.IsUpper(c)) + { + output.Append(' '); + } + + output.Append(c); + } + + return output.ToString().Trim(); + } public static string FormatUptime(double uptime) { TimeSpan t = TimeSpan.FromMilliseconds(uptime); diff --git a/Moonlight/App/Helpers/PropBinder.cs b/Moonlight/App/Helpers/PropBinder.cs new file mode 100644 index 0000000..ddb5a60 --- /dev/null +++ b/Moonlight/App/Helpers/PropBinder.cs @@ -0,0 +1,51 @@ +using System.Reflection; + +namespace Moonlight.App.Helpers; + +public class PropBinder +{ + private PropertyInfo PropertyInfo; + private object DataObject; + + public PropBinder(PropertyInfo propertyInfo, object dataObject) + { + PropertyInfo = propertyInfo; + DataObject = dataObject; + } + + public string StringValue + { + get => (string)PropertyInfo.GetValue(DataObject)!; + set => PropertyInfo.SetValue(DataObject, value); + } + + public int IntValue + { + get => (int)PropertyInfo.GetValue(DataObject)!; + set => PropertyInfo.SetValue(DataObject, value); + } + + public long LongValue + { + get => (long)PropertyInfo.GetValue(DataObject)!; + set => PropertyInfo.SetValue(DataObject, value); + } + + public bool BoolValue + { + get => (bool)PropertyInfo.GetValue(DataObject)!; + set => PropertyInfo.SetValue(DataObject, value); + } + + public DateTime DateTimeValue + { + get => (DateTime)PropertyInfo.GetValue(DataObject)!; + set => PropertyInfo.SetValue(DataObject, value); + } + + public double DoubleValue + { + get => (double)PropertyInfo.GetValue(DataObject)!; + set => PropertyInfo.SetValue(DataObject, value); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/ConfigService.cs b/Moonlight/App/Services/ConfigService.cs index f6573e1..4a9c0d7 100644 --- a/Moonlight/App/Services/ConfigService.cs +++ b/Moonlight/App/Services/ConfigService.cs @@ -51,7 +51,27 @@ public class ConfigService File.ReadAllText(path) ) ?? new ConfigV1(); - File.WriteAllText(path, JsonConvert.SerializeObject(Configuration)); + File.WriteAllText(path, JsonConvert.SerializeObject(Configuration, Formatting.Indented)); + } + + public void Save(ConfigV1 configV1) + { + Configuration = configV1; + Save(); + } + + public void Save() + { + var path = PathBuilder.File("storage", "configs", "config.json"); + + if (!File.Exists(path)) + { + File.WriteAllText(path, "{}"); + } + + File.WriteAllText(path, JsonConvert.SerializeObject(Configuration, Formatting.Indented)); + + Reload(); } public ConfigV1 Get() diff --git a/Moonlight/Shared/Components/Forms/SmartFormClass.razor b/Moonlight/Shared/Components/Forms/SmartFormClass.razor new file mode 100644 index 0000000..2eef37e --- /dev/null +++ b/Moonlight/Shared/Components/Forms/SmartFormClass.razor @@ -0,0 +1,65 @@ +@using System.Reflection +@using System.Collections +@using Moonlight.App.Helpers + +
+
+

+ +

+
+
+ @foreach (var property in Model.GetType().GetProperties()) + { + @BindAndRenderProperty(property) + } +
+
+
+
+ +@code +{ + [Parameter] + public object Model { get; set; } + + private RenderFragment BindAndRenderProperty(PropertyInfo property) + { + if (property.PropertyType.IsClass && !property.PropertyType.IsPrimitive && !typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) + { + return @; + + // If the property is a subclass, serialize and generate form for it + /* + foreach (var subProperty in property.PropertyType.GetProperties()) + { + return BindAndRenderProperty(subProperty); + }*/ + } + else if (property.PropertyType == typeof(int) || property.PropertyType == typeof(string) || property.PropertyType == typeof(bool) || property.PropertyType == typeof(decimal) || property.PropertyType == typeof(long)) + { + return @; + } + else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) + { + // If the property is a collection, generate form for each element + var collection = property.GetValue(Model) as IEnumerable; + if (collection != null) + { + foreach (var element in collection) + { + } + } + } + // Additional property types could be handled here (e.g., DateTime, int, etc.) + + return @
; + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Forms/SmartFormProperty.razor b/Moonlight/Shared/Components/Forms/SmartFormProperty.razor new file mode 100644 index 0000000..d398e88 --- /dev/null +++ b/Moonlight/Shared/Components/Forms/SmartFormProperty.razor @@ -0,0 +1,79 @@ +@using System.Reflection +@using Moonlight.App.Helpers +@using System.ComponentModel + + +@{ + //TODO: Tidy up this code + + var attrs = PropertyInfo.GetCustomAttributes(true); + + var descAttr = attrs + .FirstOrDefault(x => x.GetType() == typeof(DescriptionAttribute)); + + var blurBool = attrs.Any(x => x.GetType() == typeof(BlurAttribute)); + var blur = blurBool ? "blur-unless-hover" : ""; +} + +@if (descAttr != null) +{ + var a = descAttr as DescriptionAttribute; + +
+ @(a.Description) +
+} + +
+ @if (PropertyInfo.PropertyType == typeof(string)) + { + var binder = new PropBinder(PropertyInfo, Model!); + +
+ +
+ } + else if (PropertyInfo.PropertyType == typeof(int)) + { + var binder = new PropBinder(PropertyInfo, Model!); + + + } + else if (PropertyInfo.PropertyType == typeof(long)) + { + var binder = new PropBinder(PropertyInfo, Model!); + + + } + else if (PropertyInfo.PropertyType == typeof(bool)) + { + var binder = new PropBinder(PropertyInfo, Model!); + +
+ +
+ } + else if (PropertyInfo.PropertyType == typeof(DateTime)) + { + var binder = new PropBinder(PropertyInfo, Model!); + + + } + else if (PropertyInfo.PropertyType == typeof(decimal)) + { + var binder = new PropBinder(PropertyInfo, Model!); + + + } +
+ +@code +{ + [Parameter] + public PropertyInfo PropertyInfo { get; set; } + + [Parameter] + public object Model { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor index f4dcab1..e7f7424 100644 --- a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor @@ -39,6 +39,11 @@ News + diff --git a/Moonlight/Shared/Views/Admin/Sys/Configuration.razor b/Moonlight/Shared/Views/Admin/Sys/Configuration.razor new file mode 100644 index 0000000..067fc90 --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Sys/Configuration.razor @@ -0,0 +1,53 @@ +@page "/admin/system/configuration" + +@using Moonlight.App.Services +@using Moonlight.Shared.Components.Navigations +@using Moonlight.App.Configuration +@using Moonlight.App.Services.Interop + +@inject ConfigService ConfigService +@inject ToastService ToastService +@inject SmartTranslateService SmartTranslateService + + + + + +
+ +
+ +
+ +
+
+
+
+ +@code +{ + private ConfigV1 Config; + + private Task Load(LazyLoader lazyLoader) + { + Config = ConfigService.Get(); + + return Task.CompletedTask; + } + + private async Task OnSubmit() + { + ConfigService.Save(Config); + await ToastService.Success( + SmartTranslateService.Translate( + "Successfully saved and reloaded configuration. Some changes may take affect after a restart of moonlight" + ) + ); + } +} \ No newline at end of file