diff --git a/.gitattributes b/.gitattributes index 30d4b6dd..6fb47de9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,10 @@ # Auto detect text files and perform LF normalization * text=auto -Moonlight/wwwroot/* linguist-vendored +Moonlight/wwwroot/** linguist-vendored +Moonlight/wwwroot/assets/js/scripts.bundle.js linguist-vendored +Moonlight/wwwroot/assets/js/widgets.bundle.js linguist-vendored +Moonlight/wwwroot/assets/js/theme.js linguist-vendored +Moonlight/wwwroot/assets/css/boxicons.min.css linguist-vendored +Moonlight/wwwroot/assets/css/style.bundle.css linguist-vendored +Moonlight/wwwroot/assets/plugins/** linguist-vendored +Moonlight/wwwroot/assets/fonts/** linguist-vendored diff --git a/Moonlight/App/ApiClients/Telemetry/Requests/TelemetryData.cs b/Moonlight/App/ApiClients/Telemetry/Requests/TelemetryData.cs new file mode 100644 index 00000000..c8fafe75 --- /dev/null +++ b/Moonlight/App/ApiClients/Telemetry/Requests/TelemetryData.cs @@ -0,0 +1,11 @@ +namespace Moonlight.App.ApiClients.Telemetry.Requests; + +public class TelemetryData +{ + public string AppUrl { get; set; } = ""; + public int Servers { get; set; } + public int Nodes { get; set; } + public int Users { get; set; } + public int Databases { get; set; } + public int Webspaces { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/ApiClients/Telemetry/TelemetryApiHelper.cs b/Moonlight/App/ApiClients/Telemetry/TelemetryApiHelper.cs new file mode 100644 index 00000000..d9b07450 --- /dev/null +++ b/Moonlight/App/ApiClients/Telemetry/TelemetryApiHelper.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; +using RestSharp; + +namespace Moonlight.App.ApiClients.Telemetry; + +public class TelemetryApiHelper +{ + private readonly RestClient Client; + + public TelemetryApiHelper() + { + Client = new(); + } + + public async Task Post(string resource, object? body) + { + var request = CreateRequest(resource); + + request.Method = Method.Post; + + request.AddParameter("application/json", JsonConvert.SerializeObject(body), ParameterType.RequestBody); + + var response = await Client.ExecuteAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new TelemetryException( + $"An error occured: ({response.StatusCode}) {response.Content}", + (int)response.StatusCode + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + } + + private RestRequest CreateRequest(string resource) + { + var url = "https://telemetry.moonlightpanel.xyz/" + resource; + + var request = new RestRequest(url) + { + Timeout = 3000000 + }; + + return request; + } +} \ No newline at end of file diff --git a/Moonlight/App/ApiClients/Telemetry/TelemetryException.cs b/Moonlight/App/ApiClients/Telemetry/TelemetryException.cs new file mode 100644 index 00000000..7b66b710 --- /dev/null +++ b/Moonlight/App/ApiClients/Telemetry/TelemetryException.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace Moonlight.App.ApiClients.Telemetry; + +[Serializable] +public class TelemetryException : Exception +{ + public int StatusCode { get; set; } + + public TelemetryException() + { + } + + public TelemetryException(string message, int statusCode) : base(message) + { + StatusCode = statusCode; + } + + public TelemetryException(string message) : base(message) + { + } + + public TelemetryException(string message, Exception inner) : base(message, inner) + { + } + + protected TelemetryException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/Moonlight/App/Configuration/ConfigV1.cs b/Moonlight/App/Configuration/ConfigV1.cs index 529d2782..2935e884 100644 --- a/Moonlight/App/Configuration/ConfigV1.cs +++ b/Moonlight/App/Configuration/ConfigV1.cs @@ -1,19 +1,27 @@ -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("Auth")] public AuthData Auth { get; set; } = new(); [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(); @@ -33,8 +41,7 @@ public class ConfigV1 [JsonProperty("Subscriptions")] public SubscriptionsData Subscriptions { get; set; } = new(); - [JsonProperty("DiscordNotifications")] - public DiscordNotificationsData DiscordNotifications { get; set; } = new(); + [JsonProperty("DiscordNotifications")] public DiscordNotificationsData DiscordNotifications { get; set; } = new(); [JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new(); @@ -51,20 +58,43 @@ public class ConfigV1 { [JsonProperty("ApiKey")] public string ApiKey { get; set; } = ""; } + + public class AuthData + { + [JsonProperty("DenyLogin")] + [Description("Prevent every new login")] + public bool DenyLogin { get; set; } = false; + + [JsonProperty("DenyRegister")] + [Description("Prevent every new user to register")] + public bool DenyRegister { get; set; } = false; + } 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 @@ -72,39 +102,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 @@ -114,13 +182,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 @@ -129,7 +205,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; @@ -149,9 +227,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(); @@ -163,41 +245,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 00000000..170c2b4f --- /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/EggConverter.cs b/Moonlight/App/Helpers/EggConverter.cs new file mode 100644 index 00000000..d97647c2 --- /dev/null +++ b/Moonlight/App/Helpers/EggConverter.cs @@ -0,0 +1,71 @@ +using System.Text; +using Moonlight.App.Database.Entities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Moonlight.App.Helpers; + +public static class EggConverter +{ + public static Image Convert(string json) + { + var result = new Image(); + + var data = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(json)) + ).Build(); + + result.Allocations = 1; + result.Description = data.GetValue("description") ?? ""; + result.Uuid = Guid.NewGuid(); + result.Startup = data.GetValue("startup") ?? ""; + result.Name = data.GetValue("name") ?? "Ptero Egg"; + + foreach (var variable in data.GetSection("variables").GetChildren()) + { + result.Variables.Add(new() + { + Key = variable.GetValue("env_variable") ?? "", + DefaultValue = variable.GetValue("default_value") ?? "" + }); + } + + var configData = data.GetSection("config"); + + result.ConfigFiles = configData.GetValue("files") ?? "{}"; + + var dImagesData = JObject.Parse(json); + var dImages = (JObject)dImagesData["docker_images"]!; + + foreach (var dockerImage in dImages) + { + var di = new DockerImage() + { + Default = dockerImage.Key == dImages.Properties().Last().Name, + Name = dockerImage.Value!.ToString() + }; + + result.DockerImages.Add(di); + } + + var installSection = data.GetSection("scripts").GetSection("installation"); + + result.InstallEntrypoint = installSection.GetValue("entrypoint") ?? "bash"; + result.InstallScript = installSection.GetValue("script") ?? ""; + result.InstallDockerImage = installSection.GetValue("container") ?? ""; + + var rawJson = configData.GetValue("startup"); + + var startupData = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(rawJson!)) + ).Build(); + + result.StartupDetection = startupData.GetValue("done", "") ?? ""; + result.StopCommand = configData.GetValue("stop") ?? ""; + + result.TagsJson = "[]"; + result.BackgroundImageUrl = ""; + + return result; + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index 31834ccd..44d08f50 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 00000000..ddb5a601 --- /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/Http/Controllers/Api/Moonlight/AvatarController.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/AvatarController.cs index 4a289071..5de6ca77 100644 --- a/Moonlight/App/Http/Controllers/Api/Moonlight/AvatarController.cs +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/AvatarController.cs @@ -25,7 +25,7 @@ public class AvatarController : Controller try { - var url = GravatarController.GetImageUrl(user.Email, 100); + var url = GravatarController.GetImageUrl(user.Email.ToLower(), 100); using var client = new HttpClient(); var res = await client.GetByteArrayAsync(url); diff --git a/Moonlight/App/Models/Misc/MailTemplate.cs b/Moonlight/App/Models/Misc/MailTemplate.cs new file mode 100644 index 00000000..1d67cdcf --- /dev/null +++ b/Moonlight/App/Models/Misc/MailTemplate.cs @@ -0,0 +1,9 @@ +using Moonlight.App.Helpers.Files; + +namespace Moonlight.App.Models.Misc; + +public class MailTemplate // This is just for the blazor table at /admin/system/mail +{ + public string Name { get; set; } = ""; + public FileData File { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Background/TelemetryService.cs b/Moonlight/App/Services/Background/TelemetryService.cs new file mode 100644 index 00000000..53e1131f --- /dev/null +++ b/Moonlight/App/Services/Background/TelemetryService.cs @@ -0,0 +1,62 @@ +using Moonlight.App.ApiClients.Telemetry; +using Moonlight.App.ApiClients.Telemetry.Requests; +using Moonlight.App.Database.Entities; +using Moonlight.App.Helpers; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Background; + +public class TelemetryService +{ + private readonly IServiceScopeFactory ServiceScopeFactory; + private readonly ConfigService ConfigService; + + public TelemetryService( + ConfigService configService, + IServiceScopeFactory serviceScopeFactory) + { + ServiceScopeFactory = serviceScopeFactory; + ConfigService = configService; + + if(!ConfigService.DebugMode) + Task.Run(Run); + } + + private async Task Run() + { + var timer = new PeriodicTimer(TimeSpan.FromMinutes(15)); + + while (true) + { + using var scope = ServiceScopeFactory.CreateScope(); + + var serversRepo = scope.ServiceProvider.GetRequiredService>(); + var nodesRepo = scope.ServiceProvider.GetRequiredService>(); + var usersRepo = scope.ServiceProvider.GetRequiredService>(); + var webspacesRepo = scope.ServiceProvider.GetRequiredService>(); + var databaseRepo = scope.ServiceProvider.GetRequiredService>(); + + var apiHelper = scope.ServiceProvider.GetRequiredService(); + + try + { + await apiHelper.Post("telemetry", new TelemetryData() + { + Servers = serversRepo.Get().Count(), + Databases = databaseRepo.Get().Count(), + Nodes = nodesRepo.Get().Count(), + Users = usersRepo.Get().Count(), + Webspaces = webspacesRepo.Get().Count(), + AppUrl = ConfigService.Get().Moonlight.AppUrl + }); + } + catch (Exception e) + { + Logger.Warn("Error sending telemetry"); + Logger.Warn(e); + } + + await timer.WaitForNextTickAsync(); + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Background/TempMailService.cs b/Moonlight/App/Services/Background/TempMailService.cs new file mode 100644 index 00000000..57832d42 --- /dev/null +++ b/Moonlight/App/Services/Background/TempMailService.cs @@ -0,0 +1,37 @@ +using System.Net.Mail; +using Moonlight.App.Helpers; + +namespace Moonlight.App.Services.Background; + +public class TempMailService +{ + private string[] Domains = Array.Empty(); + + public TempMailService() + { + Task.Run(Init); + } + + private async Task Init() + { + var client = new HttpClient(); + var text = await client.GetStringAsync("https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf"); + + Domains = text + .Split("\n") + .Select(x => x.Trim()) + .ToArray(); + + Logger.Info($"Fetched {Domains.Length} temp mail domains"); + } + + public Task IsTempMail(string mail) + { + var address = new MailAddress(mail); + + if (Domains.Contains(address.Host)) + return Task.FromResult(true); + + return Task.FromResult(false); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/ConfigService.cs b/Moonlight/App/Services/ConfigService.cs index f6573e19..4a9c0d7e 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/App/Services/DomainService.cs b/Moonlight/App/Services/DomainService.cs index c56855f1..51837c5d 100644 --- a/Moonlight/App/Services/DomainService.cs +++ b/Moonlight/App/Services/DomainService.cs @@ -20,6 +20,7 @@ namespace Moonlight.App.Services; public class DomainService { private readonly DomainRepository DomainRepository; + private readonly ConfigService ConfigService; private readonly SharedDomainRepository SharedDomainRepository; private readonly CloudFlareClient Client; private readonly string AccountId; @@ -29,6 +30,7 @@ public class DomainService DomainRepository domainRepository, SharedDomainRepository sharedDomainRepository) { + ConfigService = configService; DomainRepository = domainRepository; SharedDomainRepository = sharedDomainRepository; @@ -48,6 +50,9 @@ public class DomainService public Task Create(string domain, SharedDomain sharedDomain, User user) { + if (!ConfigService.Get().Moonlight.Domains.Enable) + throw new DisplayException("This operation is disabled"); + if (DomainRepository.Get().Where(x => x.SharedDomain.Id == sharedDomain.Id).Any(x => x.Name == domain)) throw new DisplayException("A domain with this name does already exist for this shared domain"); @@ -63,6 +68,9 @@ public class DomainService public Task Delete(Domain domain) { + if (!ConfigService.Get().Moonlight.Domains.Enable) + throw new DisplayException("This operation is disabled"); + DomainRepository.Delete(domain); return Task.CompletedTask; @@ -71,6 +79,9 @@ public class DomainService public async Task GetAvailableDomains() // This method returns all available domains which are not added as a shared domain { + if (!ConfigService.Get().Moonlight.Domains.Enable) + return Array.Empty(); + var domains = GetData( await Client.Zones.GetAsync(new() { @@ -93,6 +104,9 @@ public class DomainService public async Task GetDnsRecords(Domain d) { + if (!ConfigService.Get().Moonlight.Domains.Enable) + return Array.Empty(); + var domain = EnsureData(d); var records = new List(); @@ -146,7 +160,7 @@ public class DomainService Type = record.Type }); } - else if (record.Name.EndsWith(rname)) + else if (record.Name == rname) { result.Add(new() { @@ -166,6 +180,9 @@ public class DomainService public async Task AddDnsRecord(Domain d, DnsRecord dnsRecord) { + if (!ConfigService.Get().Moonlight.Domains.Enable) + throw new DisplayException("This operation is disabled"); + var domain = EnsureData(d); var rname = $"{domain.Name}.{domain.SharedDomain.Name}"; @@ -225,6 +242,9 @@ public class DomainService public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord) { + if (!ConfigService.Get().Moonlight.Domains.Enable) + throw new DisplayException("This operation is disabled"); + var domain = EnsureData(d); var rname = $"{domain.Name}.{domain.SharedDomain.Name}"; @@ -255,6 +275,9 @@ public class DomainService public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord) { + if (!ConfigService.Get().Moonlight.Domains.Enable) + throw new DisplayException("This operation is disabled"); + var domain = EnsureData(d); GetData( diff --git a/Moonlight/App/Services/Mail/MailService.cs b/Moonlight/App/Services/Mail/MailService.cs index 23d425bf..f362b6d7 100644 --- a/Moonlight/App/Services/Mail/MailService.cs +++ b/Moonlight/App/Services/Mail/MailService.cs @@ -2,6 +2,7 @@ using Moonlight.App.Database.Entities; using Moonlight.App.Exceptions; using Moonlight.App.Helpers; +using Moonlight.App.Repositories; using SmtpClient = MailKit.Net.Smtp.SmtpClient; namespace Moonlight.App.Services.Mail; @@ -14,8 +15,14 @@ public class MailService private readonly int Port; private readonly bool Ssl; - public MailService(ConfigService configService) + private readonly Repository UserRepository; + + public MailService( + ConfigService configService, + Repository userRepository) { + UserRepository = userRepository; + var mailConfig = configService .Get() .Moonlight.Mail; @@ -26,29 +33,9 @@ public class MailService Port = mailConfig.Port; Ssl = mailConfig.Ssl; } - - public async Task SendMail( - User user, - string name, - Action> values - ) + + public Task SendMailRaw(User user, string html) { - if (!File.Exists(PathBuilder.File("storage", "resources", "mail", $"{name}.html"))) - { - Logger.Warn($"Mail template '{name}' not found. Make sure to place one in the resources folder"); - throw new DisplayException("Mail template not found"); - } - - var rawHtml = await File.ReadAllTextAsync(PathBuilder.File("storage", "resources", "mail", $"{name}.html")); - - var val = new Dictionary(); - values.Invoke(val); - - val.Add("FirstName", user.FirstName); - val.Add("LastName", user.LastName); - - var parsed = ParseMail(rawHtml, val); - Task.Run(async () => { try @@ -62,17 +49,15 @@ public class MailService var body = new BodyBuilder { - HtmlBody = parsed + HtmlBody = html }; mailMessage.Body = body.ToMessageBody(); - using (var smtpClient = new SmtpClient()) - { - await smtpClient.ConnectAsync(Server, Port, Ssl); - await smtpClient.AuthenticateAsync(Email, Password); - await smtpClient.SendAsync(mailMessage); - await smtpClient.DisconnectAsync(true); - } + using var smtpClient = new SmtpClient(); + await smtpClient.ConnectAsync(Server, Port, Ssl); + await smtpClient.AuthenticateAsync(Email, Password); + await smtpClient.SendAsync(mailMessage); + await smtpClient.DisconnectAsync(true); } catch (Exception e) { @@ -80,6 +65,54 @@ public class MailService Logger.Warn(e); } }); + + return Task.CompletedTask; + } + + public async Task SendMail(User user, string template, Action> values) + { + if (!File.Exists(PathBuilder.File("storage", "resources", "mail", $"{template}.html"))) + { + Logger.Warn($"Mail template '{template}' not found. Make sure to place one in the resources folder"); + throw new DisplayException("Mail template not found"); + } + + var rawHtml = await File.ReadAllTextAsync(PathBuilder.File("storage", "resources", "mail", $"{template}.html")); + + var val = new Dictionary(); + values.Invoke(val); + + val.Add("FirstName", user.FirstName); + val.Add("LastName", user.LastName); + + var parsed = ParseMail(rawHtml, val); + + await SendMailRaw(user, parsed); + } + + public async Task SendEmailToAll(string template, Action> values) + { + var users = UserRepository + .Get() + .ToArray(); + + foreach (var user in users) + { + await SendMail(user, template, values); + } + } + + public async Task SendEmailToAllAdmins(string template, Action> values) + { + var users = UserRepository + .Get() + .Where(x => x.Admin) + .ToArray(); + + foreach (var user in users) + { + await SendMail(user, template, values); + } } private string ParseMail(string html, Dictionary values) diff --git a/Moonlight/App/Services/Mail/TrashMailDetectorService.cs b/Moonlight/App/Services/Mail/TrashMailDetectorService.cs deleted file mode 100644 index 417f61fa..00000000 --- a/Moonlight/App/Services/Mail/TrashMailDetectorService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Net; -using Moonlight.App.Helpers; - -namespace Moonlight.App.Services.Mail; - -public class TrashMailDetectorService -{ - private string[] Domains; - - public TrashMailDetectorService() - { - Logger.Info("Fetching trash mail list from github repository"); - - using var wc = new WebClient(); - - var lines = wc - .DownloadString("https://raw.githubusercontent.com/Endelon-Hosting/TrashMailDomainDetector/main/trashmail_domains.md") - .Replace("\r\n", "\n") - .Split(new [] { "\n" }, StringSplitOptions.RemoveEmptyEntries); - - Domains = GetDomains(lines).ToArray(); - } - - private IEnumerable GetDomains(string[] lines) - { - foreach (var line in lines) - { - if (!string.IsNullOrWhiteSpace(line)) - { - if (line.Contains(".")) - { - var domain = line.Remove(0, line.IndexOf(".", StringComparison.Ordinal) + 1).Trim(); - if (domain.Contains(".")) - { - yield return domain; - } - } - } - } - } - - public bool IsTrashEmail(string mail) - { - return Domains.Contains(mail.Split('@')[1]); - } -} \ No newline at end of file diff --git a/Moonlight/App/Services/Sessions/SessionClientService.cs b/Moonlight/App/Services/Sessions/SessionClientService.cs index c152ffda..e7040ba4 100644 --- a/Moonlight/App/Services/Sessions/SessionClientService.cs +++ b/Moonlight/App/Services/Sessions/SessionClientService.cs @@ -11,6 +11,8 @@ public class SessionClientService public readonly Guid Uuid = Guid.NewGuid(); public readonly DateTime CreateTimestamp = DateTime.UtcNow; public User? User { get; private set; } + public string Ip { get; private set; } = "N/A"; + public string Device { get; private set; } = "N/A"; public readonly IdentityService IdentityService; public readonly AlertService AlertService; @@ -39,6 +41,8 @@ public class SessionClientService public async Task Start() { User = await IdentityService.Get(); + Ip = IdentityService.GetIp(); + Device = IdentityService.GetDevice(); if (User != null) // Track users last visit { diff --git a/Moonlight/App/Services/UserService.cs b/Moonlight/App/Services/UserService.cs index 0869c3c8..cab1b644 100644 --- a/Moonlight/App/Services/UserService.cs +++ b/Moonlight/App/Services/UserService.cs @@ -5,6 +5,7 @@ using Moonlight.App.Exceptions; using Moonlight.App.Helpers; using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; +using Moonlight.App.Services.Background; using Moonlight.App.Services.Mail; using Moonlight.App.Services.Sessions; @@ -18,6 +19,8 @@ public class UserService private readonly IdentityService IdentityService; private readonly IpLocateService IpLocateService; private readonly DateTimeService DateTimeService; + private readonly ConfigService ConfigService; + private readonly TempMailService TempMailService; private readonly string JwtSecret; @@ -28,14 +31,17 @@ public class UserService MailService mailService, IdentityService identityService, IpLocateService ipLocateService, - DateTimeService dateTimeService) + DateTimeService dateTimeService, + TempMailService tempMailService) { UserRepository = userRepository; TotpService = totpService; + ConfigService = configService; MailService = mailService; IdentityService = identityService; IpLocateService = ipLocateService; DateTimeService = dateTimeService; + TempMailService = tempMailService; JwtSecret = configService .Get() @@ -44,6 +50,12 @@ public class UserService public async Task Register(string email, string password, string firstname, string lastname) { + if (ConfigService.Get().Moonlight.Auth.DenyRegister) + throw new DisplayException("This operation was disabled"); + + if (await TempMailService.IsTempMail(email)) + throw new DisplayException("This email is blacklisted"); + // Check if the email is already taken var emailTaken = UserRepository.Get().FirstOrDefault(x => x.Email == email) != null; @@ -108,6 +120,9 @@ public class UserService public async Task Login(string email, string password, string totpCode = "") { + if (ConfigService.Get().Moonlight.Auth.DenyLogin) + throw new DisplayException("This operation was disabled"); + // First password check and check if totp is enabled var needTotp = await CheckTotp(email, password); diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 6a414261..60c742bd 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -5,6 +5,7 @@ using Moonlight.App.ApiClients.CloudPanel; using Moonlight.App.ApiClients.Daemon; using Moonlight.App.ApiClients.Modrinth; using Moonlight.App.ApiClients.Paper; +using Moonlight.App.ApiClients.Telemetry; using Moonlight.App.ApiClients.Wings; using Moonlight.App.Database; using Moonlight.App.Diagnostics.HealthChecks; @@ -103,8 +104,6 @@ namespace Moonlight } } - Logger.Info($"Working dir: {Directory.GetCurrentDirectory()}"); - Logger.Info("Running pre-init tasks"); var databaseCheckupService = new DatabaseCheckupService(configService); @@ -216,10 +215,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddSingleton(); - - // Loggers builder.Services.AddScoped(); - builder.Services.AddSingleton(); // Support chat builder.Services.AddSingleton(); @@ -237,6 +233,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); // Background services builder.Services.AddSingleton(); @@ -244,6 +241,8 @@ namespace Moonlight builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Other builder.Services.AddSingleton(); @@ -292,6 +291,8 @@ namespace Moonlight _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); + _ = app.Services.GetRequiredService(); + _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); diff --git a/Moonlight/Shared/Components/Forms/SmartFormClass.razor b/Moonlight/Shared/Components/Forms/SmartFormClass.razor new file mode 100644 index 00000000..2eef37e3 --- /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 00000000..d398e88f --- /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 f4dcab1f..1a92236d 100644 --- a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor @@ -39,6 +39,16 @@ News + + diff --git a/Moonlight/Shared/Views/Admin/Servers/Images/Index.razor b/Moonlight/Shared/Views/Admin/Servers/Images/Index.razor index d947bb4b..5fc6356c 100644 --- a/Moonlight/Shared/Views/Admin/Servers/Images/Index.razor +++ b/Moonlight/Shared/Views/Admin/Servers/Images/Index.razor @@ -32,7 +32,7 @@ New image