Merge pull request #226 from Moonlight-Panel/main

Update branch StripeIntegration with latest commits
This commit is contained in:
Marcel Baumgartner
2023-07-13 20:46:39 +02:00
committed by GitHub
34 changed files with 1370 additions and 315 deletions

9
.gitattributes vendored
View File

@@ -1,3 +1,10 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * 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

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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)
{
}
}

View File

@@ -1,19 +1,27 @@
namespace Moonlight.App.Configuration; using System.ComponentModel;
using Moonlight.App.Helpers;
namespace Moonlight.App.Configuration;
using System; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
public class ConfigV1 public class ConfigV1
{ {
[JsonProperty("Moonlight")] public MoonlightData Moonlight { get; set; } = new(); [JsonProperty("Moonlight")]
public MoonlightData Moonlight { get; set; } = new();
public class MoonlightData 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("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(); [JsonProperty("DiscordBot")] public DiscordBotData DiscordBot { get; set; } = new();
@@ -33,8 +41,7 @@ public class ConfigV1
[JsonProperty("Subscriptions")] public SubscriptionsData Subscriptions { get; set; } = new(); [JsonProperty("Subscriptions")] public SubscriptionsData Subscriptions { get; set; } = new();
[JsonProperty("DiscordNotifications")] [JsonProperty("DiscordNotifications")] public DiscordNotificationsData DiscordNotifications { get; set; } = new();
public DiscordNotificationsData DiscordNotifications { get; set; } = new();
[JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new(); [JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new();
@@ -52,19 +59,42 @@ public class ConfigV1
[JsonProperty("ApiKey")] public string ApiKey { get; set; } = ""; [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 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 public class DatabaseData
@@ -73,38 +103,76 @@ public class ConfigV1
[JsonProperty("Host")] public string Host { get; set; } = "your.database.host"; [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("Port")] public long Port { get; set; } = 3306;
[JsonProperty("Username")] public string Username { get; set; } = "moonlight_user"; [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 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("PowerActions")]
[JsonProperty("SendCommands")] public bool SendCommands { get; set; } = false; [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 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 public class DomainsData
{ {
[JsonProperty("Enable")] public bool Enable { get; set; } = false; [JsonProperty("Enable")]
[JsonProperty("AccountId")] public string AccountId { get; set; } = "cloudflare acc id"; [Description("This enables the domain system")]
public bool Enable { get; set; } = false;
[JsonProperty("Email")] public string Email { get; set; } = "cloudflare@acc.email"; [JsonProperty("AccountId")]
[Description("This option specifies the cloudflare account id")]
public string AccountId { get; set; } = "cloudflare acc id";
[JsonProperty("Key")] public string Key { get; set; } = "secret"; [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")]
[Description("Your cloudflare api key goes here")]
[Blur]
public string Key { get; set; } = "secret";
} }
public class HtmlData public class HtmlData
@@ -114,13 +182,21 @@ public class ConfigV1
public class HeadersData 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 public class MailData
@@ -129,7 +205,9 @@ public class ConfigV1
[JsonProperty("Server")] public string Server { get; set; } = "your.mail.host"; [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; [JsonProperty("Port")] public int Port { get; set; } = 465;
@@ -149,9 +227,13 @@ public class ConfigV1
public class OAuth2Data 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")] [JsonProperty("Providers")]
public OAuth2ProviderData[] Providers { get; set; } = Array.Empty<OAuth2ProviderData>(); public OAuth2ProviderData[] Providers { get; set; } = Array.Empty<OAuth2ProviderData>();
@@ -163,41 +245,65 @@ public class ConfigV1
[JsonProperty("ClientId")] public string ClientId { get; set; } [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 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 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(); [JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
} }
public class ReCaptchaData 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 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 public class SmartDeployData

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.Helpers;
public class BlurAttribute : Attribute
{
}

View File

@@ -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<string>("description") ?? "";
result.Uuid = Guid.NewGuid();
result.Startup = data.GetValue<string>("startup") ?? "";
result.Name = data.GetValue<string>("name") ?? "Ptero Egg";
foreach (var variable in data.GetSection("variables").GetChildren())
{
result.Variables.Add(new()
{
Key = variable.GetValue<string>("env_variable") ?? "",
DefaultValue = variable.GetValue<string>("default_value") ?? ""
});
}
var configData = data.GetSection("config");
result.ConfigFiles = configData.GetValue<string>("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<string>("entrypoint") ?? "bash";
result.InstallScript = installSection.GetValue<string>("script") ?? "";
result.InstallDockerImage = installSection.GetValue<string>("container") ?? "";
var rawJson = configData.GetValue<string>("startup");
var startupData = new ConfigurationBuilder().AddJsonStream(
new MemoryStream(Encoding.ASCII.GetBytes(rawJson!))
).Build();
result.StartupDetection = startupData.GetValue<string>("done", "") ?? "";
result.StopCommand = configData.GetValue<string>("stop") ?? "";
result.TagsJson = "[]";
result.BackgroundImageUrl = "";
return result;
}
}

View File

@@ -1,9 +1,36 @@
using Moonlight.App.Services; using System.Text;
using Moonlight.App.Services;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers;
public static class Formatter 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) public static string FormatUptime(double uptime)
{ {
TimeSpan t = TimeSpan.FromMilliseconds(uptime); TimeSpan t = TimeSpan.FromMilliseconds(uptime);

View File

@@ -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);
}
}

View File

@@ -25,7 +25,7 @@ public class AvatarController : Controller
try try
{ {
var url = GravatarController.GetImageUrl(user.Email, 100); var url = GravatarController.GetImageUrl(user.Email.ToLower(), 100);
using var client = new HttpClient(); using var client = new HttpClient();
var res = await client.GetByteArrayAsync(url); var res = await client.GetByteArrayAsync(url);

View File

@@ -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; }
}

View File

@@ -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<Repository<Server>>();
var nodesRepo = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
var usersRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
var webspacesRepo = scope.ServiceProvider.GetRequiredService<Repository<WebSpace>>();
var databaseRepo = scope.ServiceProvider.GetRequiredService<Repository<MySqlDatabase>>();
var apiHelper = scope.ServiceProvider.GetRequiredService<TelemetryApiHelper>();
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();
}
}
}

View File

@@ -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<string>();
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<bool> IsTempMail(string mail)
{
var address = new MailAddress(mail);
if (Domains.Contains(address.Host))
return Task.FromResult(true);
return Task.FromResult(false);
}
}

View File

@@ -51,7 +51,27 @@ public class ConfigService
File.ReadAllText(path) File.ReadAllText(path)
) ?? new ConfigV1(); ) ?? 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() public ConfigV1 Get()

View File

@@ -20,6 +20,7 @@ namespace Moonlight.App.Services;
public class DomainService public class DomainService
{ {
private readonly DomainRepository DomainRepository; private readonly DomainRepository DomainRepository;
private readonly ConfigService ConfigService;
private readonly SharedDomainRepository SharedDomainRepository; private readonly SharedDomainRepository SharedDomainRepository;
private readonly CloudFlareClient Client; private readonly CloudFlareClient Client;
private readonly string AccountId; private readonly string AccountId;
@@ -29,6 +30,7 @@ public class DomainService
DomainRepository domainRepository, DomainRepository domainRepository,
SharedDomainRepository sharedDomainRepository) SharedDomainRepository sharedDomainRepository)
{ {
ConfigService = configService;
DomainRepository = domainRepository; DomainRepository = domainRepository;
SharedDomainRepository = sharedDomainRepository; SharedDomainRepository = sharedDomainRepository;
@@ -48,6 +50,9 @@ public class DomainService
public Task<Domain> Create(string domain, SharedDomain sharedDomain, User user) public Task<Domain> 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)) 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"); 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) public Task Delete(Domain domain)
{ {
if (!ConfigService.Get().Moonlight.Domains.Enable)
throw new DisplayException("This operation is disabled");
DomainRepository.Delete(domain); DomainRepository.Delete(domain);
return Task.CompletedTask; return Task.CompletedTask;
@@ -71,6 +79,9 @@ public class DomainService
public async Task<Zone[]> public async Task<Zone[]>
GetAvailableDomains() // This method returns all available domains which are not added as a shared domain GetAvailableDomains() // This method returns all available domains which are not added as a shared domain
{ {
if (!ConfigService.Get().Moonlight.Domains.Enable)
return Array.Empty<Zone>();
var domains = GetData( var domains = GetData(
await Client.Zones.GetAsync(new() await Client.Zones.GetAsync(new()
{ {
@@ -93,6 +104,9 @@ public class DomainService
public async Task<DnsRecord[]> GetDnsRecords(Domain d) public async Task<DnsRecord[]> GetDnsRecords(Domain d)
{ {
if (!ConfigService.Get().Moonlight.Domains.Enable)
return Array.Empty<DnsRecord>();
var domain = EnsureData(d); var domain = EnsureData(d);
var records = new List<CloudFlare.Client.Api.Zones.DnsRecord.DnsRecord>(); var records = new List<CloudFlare.Client.Api.Zones.DnsRecord.DnsRecord>();
@@ -146,7 +160,7 @@ public class DomainService
Type = record.Type Type = record.Type
}); });
} }
else if (record.Name.EndsWith(rname)) else if (record.Name == rname)
{ {
result.Add(new() result.Add(new()
{ {
@@ -166,6 +180,9 @@ public class DomainService
public async Task AddDnsRecord(Domain d, DnsRecord dnsRecord) 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 domain = EnsureData(d);
var rname = $"{domain.Name}.{domain.SharedDomain.Name}"; var rname = $"{domain.Name}.{domain.SharedDomain.Name}";
@@ -225,6 +242,9 @@ public class DomainService
public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord) 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 domain = EnsureData(d);
var rname = $"{domain.Name}.{domain.SharedDomain.Name}"; var rname = $"{domain.Name}.{domain.SharedDomain.Name}";
@@ -255,6 +275,9 @@ public class DomainService
public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord) 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); var domain = EnsureData(d);
GetData( GetData(

View File

@@ -2,6 +2,7 @@
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Repositories;
using SmtpClient = MailKit.Net.Smtp.SmtpClient; using SmtpClient = MailKit.Net.Smtp.SmtpClient;
namespace Moonlight.App.Services.Mail; namespace Moonlight.App.Services.Mail;
@@ -14,8 +15,14 @@ public class MailService
private readonly int Port; private readonly int Port;
private readonly bool Ssl; private readonly bool Ssl;
public MailService(ConfigService configService) private readonly Repository<User> UserRepository;
public MailService(
ConfigService configService,
Repository<User> userRepository)
{ {
UserRepository = userRepository;
var mailConfig = configService var mailConfig = configService
.Get() .Get()
.Moonlight.Mail; .Moonlight.Mail;
@@ -27,28 +34,8 @@ public class MailService
Ssl = mailConfig.Ssl; Ssl = mailConfig.Ssl;
} }
public async Task SendMail( public Task SendMailRaw(User user, string html)
User user,
string name,
Action<Dictionary<string, string>> values
)
{ {
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<string, string>();
values.Invoke(val);
val.Add("FirstName", user.FirstName);
val.Add("LastName", user.LastName);
var parsed = ParseMail(rawHtml, val);
Task.Run(async () => Task.Run(async () =>
{ {
try try
@@ -62,24 +49,70 @@ public class MailService
var body = new BodyBuilder var body = new BodyBuilder
{ {
HtmlBody = parsed HtmlBody = html
}; };
mailMessage.Body = body.ToMessageBody(); mailMessage.Body = body.ToMessageBody();
using (var smtpClient = new SmtpClient()) using var smtpClient = new SmtpClient();
{
await smtpClient.ConnectAsync(Server, Port, Ssl); await smtpClient.ConnectAsync(Server, Port, Ssl);
await smtpClient.AuthenticateAsync(Email, Password); await smtpClient.AuthenticateAsync(Email, Password);
await smtpClient.SendAsync(mailMessage); await smtpClient.SendAsync(mailMessage);
await smtpClient.DisconnectAsync(true); await smtpClient.DisconnectAsync(true);
} }
}
catch (Exception e) catch (Exception e)
{ {
Logger.Warn("Error sending mail"); Logger.Warn("Error sending mail");
Logger.Warn(e); Logger.Warn(e);
} }
}); });
return Task.CompletedTask;
}
public async Task SendMail(User user, string template, Action<Dictionary<string, string>> 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<string, string>();
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<Dictionary<string, string>> values)
{
var users = UserRepository
.Get()
.ToArray();
foreach (var user in users)
{
await SendMail(user, template, values);
}
}
public async Task SendEmailToAllAdmins(string template, Action<Dictionary<string, string>> 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<string, string> values) private string ParseMail(string html, Dictionary<string, string> values)

View File

@@ -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<string> 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]);
}
}

View File

@@ -11,6 +11,8 @@ public class SessionClientService
public readonly Guid Uuid = Guid.NewGuid(); public readonly Guid Uuid = Guid.NewGuid();
public readonly DateTime CreateTimestamp = DateTime.UtcNow; public readonly DateTime CreateTimestamp = DateTime.UtcNow;
public User? User { get; private set; } 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 IdentityService IdentityService;
public readonly AlertService AlertService; public readonly AlertService AlertService;
@@ -39,6 +41,8 @@ public class SessionClientService
public async Task Start() public async Task Start()
{ {
User = await IdentityService.Get(); User = await IdentityService.Get();
Ip = IdentityService.GetIp();
Device = IdentityService.GetDevice();
if (User != null) // Track users last visit if (User != null) // Track users last visit
{ {

View File

@@ -5,6 +5,7 @@ using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services.Background;
using Moonlight.App.Services.Mail; using Moonlight.App.Services.Mail;
using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Sessions;
@@ -18,6 +19,8 @@ public class UserService
private readonly IdentityService IdentityService; private readonly IdentityService IdentityService;
private readonly IpLocateService IpLocateService; private readonly IpLocateService IpLocateService;
private readonly DateTimeService DateTimeService; private readonly DateTimeService DateTimeService;
private readonly ConfigService ConfigService;
private readonly TempMailService TempMailService;
private readonly string JwtSecret; private readonly string JwtSecret;
@@ -28,14 +31,17 @@ public class UserService
MailService mailService, MailService mailService,
IdentityService identityService, IdentityService identityService,
IpLocateService ipLocateService, IpLocateService ipLocateService,
DateTimeService dateTimeService) DateTimeService dateTimeService,
TempMailService tempMailService)
{ {
UserRepository = userRepository; UserRepository = userRepository;
TotpService = totpService; TotpService = totpService;
ConfigService = configService;
MailService = mailService; MailService = mailService;
IdentityService = identityService; IdentityService = identityService;
IpLocateService = ipLocateService; IpLocateService = ipLocateService;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
TempMailService = tempMailService;
JwtSecret = configService JwtSecret = configService
.Get() .Get()
@@ -44,6 +50,12 @@ public class UserService
public async Task<string> Register(string email, string password, string firstname, string lastname) public async Task<string> 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 // Check if the email is already taken
var emailTaken = UserRepository.Get().FirstOrDefault(x => x.Email == email) != null; var emailTaken = UserRepository.Get().FirstOrDefault(x => x.Email == email) != null;
@@ -108,6 +120,9 @@ public class UserService
public async Task<string> Login(string email, string password, string totpCode = "") public async Task<string> 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 // First password check and check if totp is enabled
var needTotp = await CheckTotp(email, password); var needTotp = await CheckTotp(email, password);

View File

@@ -5,6 +5,7 @@ using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.ApiClients.Daemon; using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Modrinth; using Moonlight.App.ApiClients.Modrinth;
using Moonlight.App.ApiClients.Paper; using Moonlight.App.ApiClients.Paper;
using Moonlight.App.ApiClients.Telemetry;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database; using Moonlight.App.Database;
using Moonlight.App.Diagnostics.HealthChecks; using Moonlight.App.Diagnostics.HealthChecks;
@@ -103,8 +104,6 @@ namespace Moonlight
} }
} }
Logger.Info($"Working dir: {Directory.GetCurrentDirectory()}");
Logger.Info("Running pre-init tasks"); Logger.Info("Running pre-init tasks");
var databaseCheckupService = new DatabaseCheckupService(configService); var databaseCheckupService = new DatabaseCheckupService(configService);
@@ -216,10 +215,7 @@ namespace Moonlight
builder.Services.AddScoped<SessionClientService>(); builder.Services.AddScoped<SessionClientService>();
builder.Services.AddSingleton<SessionServerService>(); builder.Services.AddSingleton<SessionServerService>();
// Loggers
builder.Services.AddScoped<MailService>(); builder.Services.AddScoped<MailService>();
builder.Services.AddSingleton<TrashMailDetectorService>();
// Support chat // Support chat
builder.Services.AddSingleton<SupportChatServerService>(); builder.Services.AddSingleton<SupportChatServerService>();
@@ -237,6 +233,7 @@ namespace Moonlight
builder.Services.AddScoped<DaemonApiHelper>(); builder.Services.AddScoped<DaemonApiHelper>();
builder.Services.AddScoped<CloudPanelApiHelper>(); builder.Services.AddScoped<CloudPanelApiHelper>();
builder.Services.AddScoped<ModrinthApiHelper>(); builder.Services.AddScoped<ModrinthApiHelper>();
builder.Services.AddScoped<TelemetryApiHelper>();
// Background services // Background services
builder.Services.AddSingleton<DiscordBotService>(); builder.Services.AddSingleton<DiscordBotService>();
@@ -244,6 +241,8 @@ namespace Moonlight
builder.Services.AddSingleton<DiscordNotificationService>(); builder.Services.AddSingleton<DiscordNotificationService>();
builder.Services.AddSingleton<CleanupService>(); builder.Services.AddSingleton<CleanupService>();
builder.Services.AddSingleton<MalwareScanService>(); builder.Services.AddSingleton<MalwareScanService>();
builder.Services.AddSingleton<TelemetryService>();
builder.Services.AddSingleton<TempMailService>();
// Other // Other
builder.Services.AddSingleton<MoonlightService>(); builder.Services.AddSingleton<MoonlightService>();
@@ -292,6 +291,8 @@ namespace Moonlight
_ = app.Services.GetRequiredService<StatisticsCaptureService>(); _ = app.Services.GetRequiredService<StatisticsCaptureService>();
_ = app.Services.GetRequiredService<DiscordNotificationService>(); _ = app.Services.GetRequiredService<DiscordNotificationService>();
_ = app.Services.GetRequiredService<MalwareScanService>(); _ = app.Services.GetRequiredService<MalwareScanService>();
_ = app.Services.GetRequiredService<TelemetryService>();
_ = app.Services.GetRequiredService<TempMailService>();
_ = app.Services.GetRequiredService<MoonlightService>(); _ = app.Services.GetRequiredService<MoonlightService>();

View File

@@ -0,0 +1,65 @@
@using System.Reflection
@using System.Collections
@using Moonlight.App.Helpers
<div class="accordion my-3" id="configSetting@(Model.GetHashCode())">
<div class="accordion-item">
<h2 class="accordion-header" id="configSetting-header@(Model.GetHashCode())">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#configSetting-body@(Model.GetHashCode())" aria-expanded="false" aria-controls="configSetting-body@(Model.GetHashCode())">
@{
var name = Formatter.ReplaceEnd(Model.GetType().Name, "Data", "");
name = Formatter.ConvertCamelCaseToSpaces(name);
}
@(name)
</button>
</h2>
<div id="configSetting-body@(Model.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="configSetting-header@(Model.GetHashCode())" data-bs-parent="#configSetting">
<div class="accordion-body">
@foreach (var property in Model.GetType().GetProperties())
{
@BindAndRenderProperty(property)
}
</div>
</div>
</div>
</div>
@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 @<SmartFormClass Model="@property.GetValue(Model)"/>;
// 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 @<SmartFormProperty Model="Model" PropertyInfo="property"/>;
}
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 @<div></div>;
}
}

View File

@@ -0,0 +1,79 @@
@using System.Reflection
@using Moonlight.App.Helpers
@using System.ComponentModel
<label class="form-label" for="@PropertyInfo.Name">
@(Formatter.ConvertCamelCaseToSpaces(PropertyInfo.Name))
</label>
@{
//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;
<div class="form-text fs-5 mb-2 mt-0">
@(a.Description)
</div>
}
<div class="input-group mb-5">
@if (PropertyInfo.PropertyType == typeof(string))
{
var binder = new PropBinder(PropertyInfo, Model!);
<div class="@(blur) w-100">
<InputText id="@PropertyInfo.Name" @bind-Value="binder.StringValue" class="form-control"/>
</div>
}
else if (PropertyInfo.PropertyType == typeof(int))
{
var binder = new PropBinder(PropertyInfo, Model!);
<InputNumber id="@PropertyInfo.Name" @bind-Value="binder.IntValue" class="form-control"/>
}
else if (PropertyInfo.PropertyType == typeof(long))
{
var binder = new PropBinder(PropertyInfo, Model!);
<InputNumber id="@PropertyInfo.Name" @bind-Value="binder.LongValue" class="form-control"/>
}
else if (PropertyInfo.PropertyType == typeof(bool))
{
var binder = new PropBinder(PropertyInfo, Model!);
<div class="form-check">
<InputCheckbox id="@PropertyInfo.Name" @bind-Value="binder.BoolValue" class="form-check-input"/>
</div>
}
else if (PropertyInfo.PropertyType == typeof(DateTime))
{
var binder = new PropBinder(PropertyInfo, Model!);
<InputDate id="@PropertyInfo.Name" @bind-Value="binder.DateTimeValue" class="form-control"/>
}
else if (PropertyInfo.PropertyType == typeof(decimal))
{
var binder = new PropBinder(PropertyInfo, Model!);
<InputNumber id="@PropertyInfo.Name" step="0.01" @bind-Value="binder.DoubleValue" class="form-control"/>
}
</div>
@code
{
[Parameter]
public PropertyInfo PropertyInfo { get; set; }
[Parameter]
public object Model { get; set; }
}

View File

@@ -39,6 +39,16 @@
<TL>News</TL> <TL>News</TL>
</a> </a>
</li> </li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 8 ? "active" : "")" href="/admin/system/configuration">
<TL>Configuration</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 9 ? "active" : "")" href="/admin/system/mail">
<TL>Mail</TL>
</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -32,7 +32,7 @@
<TL>New image</TL> <TL>New image</TL>
</a> </a>
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden="" multiple=""/> <InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden="" multiple=""/>
<label for="fileUpload" class="btn btn-sm btn-light-primary"> <label for="fileUpload" class="btn btn-sm btn-light-primary me-3">
<span class="svg-icon svg-icon-2"> <span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path> <path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
@@ -42,6 +42,17 @@
</span> </span>
<TL>Import</TL> <TL>Import</TL>
</label> </label>
<InputFile OnChange="OnEggFileChanged" type="file" id="eggFileUpload" hidden="" multiple=""/>
<label for="eggFileUpload" class="btn btn-sm btn-light-primary">
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
</svg>
</span>
<TL>Import pterodactyl egg</TL>
</label>
</div> </div>
</div> </div>
<div class="card-body pt-0"> <div class="card-body pt-0">
@@ -173,4 +184,44 @@
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task OnEggFileChanged(InputFileChangeEventArgs arg)
{
var b = await AlertService.YesNo(
SmartTranslateService.Translate("Attention"),
SmartTranslateService.Translate("Imported pterodactyl eggs can result in broken images. We do not support pterodactyl eggs"),
SmartTranslateService.Translate("I take the risk"),
SmartTranslateService.Translate("Cancel")
);
if(!b)
return;
foreach (var browserFile in arg.GetMultipleFiles())
{
try
{
var stream = browserFile.OpenReadStream(1024 * 1024 * 100);
var data = new byte[browserFile.Size];
_ = await stream.ReadAsync(data, 0, data.Length);
var json = Encoding.UTF8.GetString(data);
var image = EggConverter.Convert(json);
ImageRepository.Add(image);
await AlertService.Success(SmartTranslateService.Translate("Successfully imported image"));
await LazyLoader.Reload();
}
catch (Exception e)
{
await AlertService.Error(SmartTranslateService.Translate("An unknown error occured while uploading and importing the image"));
Logger.Error("Error importing image");
Logger.Error(e);
}
}
await InvokeAsync(StateHasChanged);
}
} }

View File

@@ -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
<OnlyAdmin>
<AdminSystemNavigation Index="8"/>
<LazyLoader Load="Load">
<div class="card">
<SmartForm Model="Config" OnValidSubmit="OnSubmit">
<div class="card-body">
<SmartFormClass Model="Config"/>
</div>
<div class="card-footer">
<div class="text-end">
<button type="submit" class="btn btn-success">
<TL>Save</TL>
</button>
</div>
</div>
</SmartForm>
</div>
</LazyLoader>
</OnlyAdmin>
@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"
)
);
}
}

View File

@@ -0,0 +1,180 @@
@page "/admin/system/mail"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Helpers
@using BlazorTable
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Misc
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using Moonlight.App.Services.Mail
@inject SmartTranslateService SmartTranslateService
@inject ToastService ToastService
@inject AlertService AlertService
@inject MailService MailService
<OnlyAdmin>
<AdminSystemNavigation Index="9"/>
<div class="card mb-3">
<div class="card-header">
<span class="card-title">
<TL>Actions</TL>
</span>
</div>
<div class="card-body">
<WButton Text="@(SmartTranslateService.Translate("Test mail configuration"))"
WorkingText="@(SmartTranslateService.Translate("Sending test mail"))"
CssClasses="btn-primary"
OnClick="SendTestMail">
</WButton>
</div>
</div>
<LazyLoader @ref="LazyLoader" Load="Load">
@if (CurrentMailTemplate == null)
{
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Mail templates</TL>
</span>
<div class="card-toolbar">
<WButton Text="@(SmartTranslateService.Translate("New mail template"))"
CssClasses="btn-sm btn-success"
OnClick="CreateNewMailTemplate">
</WButton>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<Table TableItem="MailTemplate" Items="MailTemplateFiles" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="MailTemplate" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true">
<Template>
@{
var name = context.Name.Replace(Path.GetExtension(context.Name), "");
}
<span>@(name)</span>
</Template>
</Column>
<Column TableItem="MailTemplate" Title="" Field="@(x => x.Name)" Filterable="false" Sortable="false">
<Template>
<div class="text-end">
<WButton Text="@(SmartTranslateService.Translate("Edit"))"
OnClick="() => EditTemplate(context)">
</WButton>
<DeleteButton OnClick="() => DeleteTemplate(context)"
Confirm="true">
</DeleteButton>
</div>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</div>
</div>
}
else
{
<FileEditor Language="html"
HideControls="false"
InitialData="@(CurrentMailTemplateContent)"
OnCancel="OnCancelTemplateEdit"
OnSubmit="OnSubmitTemplateEdit"/>
}
</LazyLoader>
</OnlyAdmin>
@code
{
[CascadingParameter]
public User User { get; set; }
private MailTemplate[] MailTemplateFiles;
private FileAccess FileAccess;
private LazyLoader LazyLoader;
#region Template Editor
private MailTemplate? CurrentMailTemplate;
private string CurrentMailTemplateContent = "";
private async Task Load(LazyLoader arg)
{
FileAccess = new HostFileAccess(PathBuilder.Dir("storage"));
await FileAccess.Cd("resources");
await FileAccess.Cd("mail");
MailTemplateFiles = (await FileAccess.Ls())
.Where(x => x.IsFile)
.Select(x => new MailTemplate()
{
Name = x.Name,
File = x
})
.ToArray();
}
private async Task EditTemplate(MailTemplate mailTemplate)
{
CurrentMailTemplate = mailTemplate;
CurrentMailTemplateContent = await FileAccess
.Read(CurrentMailTemplate.File);
await InvokeAsync(StateHasChanged);
}
private async Task DeleteTemplate(MailTemplate mailTemplate)
{
await FileAccess.Delete(mailTemplate.File);
await LazyLoader.Reload();
}
private async void OnCancelTemplateEdit()
{
CurrentMailTemplate = null;
await InvokeAsync(StateHasChanged);
}
private async void OnSubmitTemplateEdit(string text)
{
await FileAccess.Write(CurrentMailTemplate!.File, text);
await ToastService.Success(
SmartTranslateService.Translate("Successfully saved file"));
}
private async Task CreateNewMailTemplate()
{
var name = await AlertService.Text(
SmartTranslateService.Translate("New mail template"),
SmartTranslateService.Translate("Enter the name of the new template"),
""
);
if (string.IsNullOrEmpty(name))
return;
await FileAccess.Write(new()
{
Name = name + ".html"
}, "");
await LazyLoader.Reload();
}
#endregion
private async Task SendTestMail()
{
await MailService.SendMailRaw(User, "<html><body>If you see this mail, your moonlight mail configuration is ready to use</body></html>");
await AlertService.Info(SmartTranslateService.Translate("A test mail has been sent to the email address of your account"));
}
}

View File

@@ -60,13 +60,13 @@
</Column> </Column>
<Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("IP"))" Field="@(x => x.Uuid)" Sortable="false" Filterable="false" Width="10%"> <Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("IP"))" Field="@(x => x.Uuid)" Sortable="false" Filterable="false" Width="10%">
<Template> <Template>
@(context.IdentityService.GetIp()) @(context.Ip)
</Template> </Template>
</Column> </Column>
<Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("URL"))" Field="@(x => x.NavigationManager.Uri)" Sortable="true" Filterable="true" Width="10%"/> <Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("URL"))" Field="@(x => x.NavigationManager.Uri)" Sortable="true" Filterable="true" Width="10%"/>
<Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("Device"))" Field="@(x => x.Uuid)" Sortable="false" Filterable="false" Width="10%"> <Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("Device"))" Field="@(x => x.Uuid)" Sortable="false" Filterable="false" Width="10%">
<Template> <Template>
@(context.IdentityService.GetDevice()) @(context.Device)
</Template> </Template>
</Column> </Column>
<Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("Time"))" Field="@(x => x.CreateTimestamp)" Sortable="true" Filterable="true" Width="10%"> <Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("Time"))" Field="@(x => x.CreateTimestamp)" Sortable="true" Filterable="true" Width="10%">

View File

@@ -26,7 +26,7 @@
<LazyLoader Load="LoadData"> <LazyLoader Load="LoadData">
@if (CurrentServer == null) @if (CurrentServer == null)
{ {
<NotFoundAlert /> <NotFoundAlert/>
} }
else else
{ {
@@ -34,34 +34,17 @@
{ {
if (Console.ConsoleState == ConsoleState.Connected) if (Console.ConsoleState == ConsoleState.Connected)
{ {
if (Console.ServerState == ServerState.Installing) if (Console.ServerState == ServerState.Installing || CurrentServer.Installing)
{ {
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="mb-10"> <div class="fs-2hx fw-bold text-gray-800 text-center mb-5">
<div class="fs-2hx fw-bold text-gray-800 text-center mb-13">
<span class="me-2">
<TL>Server installation is currently running</TL> <TL>Server installation is currently running</TL>
</span>
</div>
</div> </div>
<div class="rounded bg-black p-3">
<Terminal @ref="InstallConsole"></Terminal> <Terminal @ref="InstallConsole"></Terminal>
</div> </div>
</div> </div>
}
else if (CurrentServer.Installing)
{
<div class="card">
<div class="card-body">
<div class="mb-10">
<div class="fs-2hx fw-bold text-gray-800 text-center mb-13">
<span class="me-2">
<TL>Server installation is currently running</TL>
</span>
</div>
</div>
<Terminal @ref="InstallConsole"></Terminal>
</div>
</div> </div>
} }
else if (CurrentServer.IsArchived) else if (CurrentServer.IsArchived)

View File

@@ -25,11 +25,17 @@
</div> </div>
<div class="col-4 d-flex flex-column flex-end mb-1"> <div class="col-4 d-flex flex-column flex-end mb-1">
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<button class="w-100 nav-link btn btn-sm btn-success fw-bold px-4 me-1 @(Console.ServerState == ServerState.Offline ? "" : "disabled")" aria-selected="true" role="tab" @onclick="Start"><TL>Start</TL></button> <button class="w-100 nav-link btn btn-sm btn-success fw-bold px-4 me-1 @(Console.ServerState == ServerState.Offline ? "" : "disabled")" aria-selected="true" role="tab" @onclick="Start">
<button class="w-100 nav-link btn btn-sm btn-primary fw-bold px-4 me-1 @(Console.ServerState == ServerState.Running ? "" : "disabled")" aria-selected="true" role="tab" @onclick="Restart"><TL>Restart</TL></button> <TL>Start</TL>
</button>
<button class="w-100 nav-link btn btn-sm btn-primary fw-bold px-4 me-1 @(Console.ServerState == ServerState.Running ? "" : "disabled")" aria-selected="true" role="tab" @onclick="Restart">
<TL>Restart</TL>
</button>
@if (Console.ServerState == ServerState.Stopping) @if (Console.ServerState == ServerState.Stopping)
{ {
<button class="w-100 nav-link btn btn-sm btn-danger fw-bold px-4 me-1" aria-selected="true" role="tab" @onclick="Kill"><TL>Kill</TL></button> <button class="w-100 nav-link btn btn-sm btn-danger fw-bold px-4 me-1" aria-selected="true" role="tab" @onclick="Kill">
<TL>Kill</TL>
</button>
} }
else else
{ {
@@ -47,7 +53,7 @@
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col fs-5"> <div class="col fs-5">
<span class="fw-bold"><TL>Shared IP</TL>:</span> <span class="fw-bold"><TL>Shared IP</TL>:</span>
<span class="ms-1 text-muted @(User.StreamerMode ? "blur" : "")">@($"{CurrentServer.Node.Fqdn}:{CurrentServer.MainAllocation.Port}")</span> <span class="ms-1 text-muted @(User.StreamerMode ? "blur" : "")">@($"{CurrentServer.Node.Fqdn}:{CurrentServer.MainAllocation?.Port ?? 0}")</span>
</div> </div>
<div class="col fs-5"> <div class="col fs-5">
<span class="fw-bold"><TL>Server ID</TL>:</span> <span class="fw-bold"><TL>Server ID</TL>:</span>
@@ -68,21 +74,29 @@
@switch (Console.ServerState) @switch (Console.ServerState)
{ {
case ServerState.Offline: case ServerState.Offline:
<span class="text-danger"><TL>Offline</TL></span> <span class="text-danger">
<TL>Offline</TL>
</span>
break; break;
case ServerState.Starting: case ServerState.Starting:
<span class="text-warning"><TL>Starting</TL></span> <span class="text-warning">
<TL>Starting</TL>
</span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span> <span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break; break;
case ServerState.Stopping: case ServerState.Stopping:
<span class="text-warning"><TL>Stopping</TL></span> <span class="text-warning">
<TL>Stopping</TL>
</span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span> <span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break; break;
case ServerState.Running: case ServerState.Running:
<span class="text-success"><TL>Online</TL></span> <span class="text-success">
<TL>Online</TL>
</span>
<span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span> <span class="text-gray-700 pt-1 fw-semibold">(@(Formatter.FormatUptime(Console.Resource.Uptime)))</span>
break; break;
} }
@@ -110,7 +124,9 @@
<a href="/server/@(CurrentServer.Uuid)/" class="nav-link w-100 btn btn-flex @(Index == 0 ? "active" : "") btn-active-light-primary"> <a href="/server/@(CurrentServer.Uuid)/" class="nav-link w-100 btn btn-flex @(Index == 0 ? "active" : "") btn-active-light-primary">
<i class="bx bx-terminal bx-sm me-2"></i> <i class="bx bx-terminal bx-sm me-2"></i>
<span class="d-flex flex-column align-items-start"> <span class="d-flex flex-column align-items-start">
<span class="fs-5"><TL>Console</TL></span> <span class="fs-5">
<TL>Console</TL>
</span>
</span> </span>
</a> </a>
</li> </li>
@@ -118,7 +134,9 @@
<a href="/server/@(CurrentServer.Uuid)/files" class="nav-link w-100 btn btn-flex @(Index == 1 ? "active" : "") btn-active-light-primary"> <a href="/server/@(CurrentServer.Uuid)/files" class="nav-link w-100 btn btn-flex @(Index == 1 ? "active" : "") btn-active-light-primary">
<i class="bx bx-folder bx-sm me-2"></i> <i class="bx bx-folder bx-sm me-2"></i>
<span class="d-flex flex-column align-items-start"> <span class="d-flex flex-column align-items-start">
<span class="fs-5"><TL>Files</TL></span> <span class="fs-5">
<TL>Files</TL>
</span>
</span> </span>
</a> </a>
</li> </li>
@@ -126,7 +144,9 @@
<a href="/server/@(CurrentServer.Uuid)/backups" class="nav-link w-100 btn btn-flex @(Index == 2 ? "active" : "") btn-active-light-primary"> <a href="/server/@(CurrentServer.Uuid)/backups" class="nav-link w-100 btn btn-flex @(Index == 2 ? "active" : "") btn-active-light-primary">
<i class="bx bx-box bx-sm me-2"></i> <i class="bx bx-box bx-sm me-2"></i>
<span class="d-flex flex-column align-items-start"> <span class="d-flex flex-column align-items-start">
<span class="fs-5"><TL>Backups</TL></span> <span class="fs-5">
<TL>Backups</TL>
</span>
</span> </span>
</a> </a>
</li> </li>
@@ -134,7 +154,9 @@
<a href="/server/@(CurrentServer.Uuid)/network" class="nav-link w-100 btn btn-flex @(Index == 3 ? "active" : "") btn-active-light-primary"> <a href="/server/@(CurrentServer.Uuid)/network" class="nav-link w-100 btn btn-flex @(Index == 3 ? "active" : "") btn-active-light-primary">
<i class="bx bx-wifi bx-sm me-2"></i> <i class="bx bx-wifi bx-sm me-2"></i>
<span class="d-flex flex-column align-items-start"> <span class="d-flex flex-column align-items-start">
<span class="fs-5"><TL>Network</TL></span> <span class="fs-5">
<TL>Network</TL>
</span>
</span> </span>
</a> </a>
</li> </li>
@@ -142,7 +164,9 @@
<a href="/server/@(CurrentServer.Uuid)/addons" class="nav-link w-100 btn btn-flex @(Index == 4 ? "active" : "") btn-active-light-primary"> <a href="/server/@(CurrentServer.Uuid)/addons" class="nav-link w-100 btn btn-flex @(Index == 4 ? "active" : "") btn-active-light-primary">
<i class="bx bx-plug bx-sm me-2"></i> <i class="bx bx-plug bx-sm me-2"></i>
<span class="d-flex flex-column align-items-start"> <span class="d-flex flex-column align-items-start">
<span class="fs-5"><TL>Addons</TL></span> <span class="fs-5">
<TL>Addons</TL>
</span>
</span> </span>
</a> </a>
</li> </li>
@@ -150,7 +174,9 @@
<a href="/server/@(CurrentServer.Uuid)/settings" class="nav-link w-100 btn btn-flex @(Index == 5 ? "active" : "") btn-active-light-primary"> <a href="/server/@(CurrentServer.Uuid)/settings" class="nav-link w-100 btn btn-flex @(Index == 5 ? "active" : "") btn-active-light-primary">
<i class="bx bx-cog bx-sm me-2"></i> <i class="bx bx-cog bx-sm me-2"></i>
<span class="d-flex flex-column align-items-start"> <span class="d-flex flex-column align-items-start">
<span class="fs-5"><TL>Settings</TL></span> <span class="fs-5">
<TL>Settings</TL>
</span>
</span> </span>
</a> </a>
</li> </li>

View File

@@ -48,7 +48,9 @@
<div class="card-body pt-0"> <div class="card-body pt-0">
<div class="d-flex flex-column gap-10"> <div class="d-flex flex-column gap-10">
<div class="fv-row"> <div class="fv-row">
<label class="form-label"><TL>Node</TL></label> <label class="form-label">
<TL>Node</TL>
</label>
<div class="fw-bold fs-3">@(DeployNode.Name)</div> <div class="fw-bold fs-3">@(DeployNode.Name)</div>
</div> </div>
@if (Model.Image != null) @if (Model.Image != null)
@@ -56,12 +58,16 @@
var limit = Images[Model.Image]; var limit = Images[Model.Image];
<div class="fv-row"> <div class="fv-row">
<label class="form-label"><TL>Image</TL></label> <label class="form-label">
<TL>Image</TL>
</label>
<div class="fw-bold fs-3">@(Model.Image.Name)</div> <div class="fw-bold fs-3">@(Model.Image.Name)</div>
</div> </div>
<div class="fv-row"> <div class="fv-row">
<label class="form-label"><TL>CPU</TL></label> <label class="form-label">
<TL>CPU</TL>
</label>
<div class="fw-bold fs-3"> <div class="fw-bold fs-3">
@{ @{
var cpu = limit.ReadValue("cpu"); var cpu = limit.ReadValue("cpu");
@@ -76,12 +82,16 @@
</div> </div>
<div class="fv-row"> <div class="fv-row">
<label class="form-label"><TL>Memory</TL></label> <label class="form-label">
<TL>Memory</TL>
</label>
<div class="fw-bold fs-3">@(limit.ReadValue("memory")) MB</div> <div class="fw-bold fs-3">@(limit.ReadValue("memory")) MB</div>
</div> </div>
<div class="fv-row"> <div class="fv-row">
<label class="form-label"><TL>Disk</TL></label> <label class="form-label">
<TL>Disk</TL>
</label>
<div class="fw-bold fs-3">@(limit.ReadValue("disk")) MB</div> <div class="fw-bold fs-3">@(limit.ReadValue("disk")) MB</div>
</div> </div>
} }
@@ -108,15 +118,6 @@
</div> </div>
@if (Images.Any()) @if (Images.Any())
{ {
<label class="form-label">
<TL>Image</TL>
</label>
<SmartSelect TField="Image"
@bind-Value="Model.Image"
Items="Images.Keys.ToArray()"
DisplayField="@(x => x.Name)">
</SmartSelect>
<button type="submit" class="mt-5 float-end btn btn-primary"> <button type="submit" class="mt-5 float-end btn btn-primary">
<TL>Create</TL> <TL>Create</TL>
</button> </button>
@@ -125,13 +126,45 @@
{ {
<div class="alert alert-warning d-flex align-items-center p-5 mb-10"> <div class="alert alert-warning d-flex align-items-center p-5 mb-10">
<span> <span>
<TL>You reached the maximum amount of servers for every image of your subscription</TL>: @(Subscription == null ? SmartTranslateService.Translate("Default") : Subscription.Name) <TL>We could not find any image in your subscription you have access to</TL>: @(Subscription == null ? SmartTranslateService.Translate("Default") : Subscription.Name)
</span> </span>
</div> </div>
} }
</SmartForm> </SmartForm>
</div> </div>
</div> </div>
<div class="row">
@foreach (var keyValuePair in Images)
{
bool selected = Model.Image != null && Model.Image.Id == keyValuePair.Key.Id;
<div class="col-12 col-md-4">
@if (ServerCounts[keyValuePair.Key] > keyValuePair.Value.Amount)
{
<div class="m-2 card blur">
<div class="card-body">
<h5 class="card-title text-center">@(keyValuePair.Key.Name)</h5>
<p class="card-text">
@(keyValuePair.Key.Description)
</p>
</div>
</div>
}
else
{
<a href="#" class="m-2 card @(selected ? "border border-2 border-primary" : "") invisible-a" @onclick:preventDefault @onclick="() => SelectImage(keyValuePair.Key)">
<div class="card-body">
<h5 class="card-title text-center">@(keyValuePair.Key.Name)</h5>
<p class="card-text">
@(keyValuePair.Key.Description)
</p>
</div>
</a>
}
</div>
}
</div>
</div> </div>
</div> </div>
} }
@@ -146,6 +179,7 @@
private Subscription? Subscription; private Subscription? Subscription;
private Dictionary<Image, SubscriptionLimit> Images = new(); private Dictionary<Image, SubscriptionLimit> Images = new();
private Dictionary<Image, int> ServerCounts = new();
private ServerOrderDataModel Model = new(); private ServerOrderDataModel Model = new();
@@ -177,8 +211,8 @@
.Where(x => x.Owner.Id == User.Id) .Where(x => x.Owner.Id == User.Id)
.Count(x => x.Image.Id == image.Id); .Count(x => x.Image.Id == image.Id);
if(serversCount < limit.Amount)
Images.Add(image, limit); Images.Add(image, limit);
ServerCounts.Add(image, serversCount);
} }
} }
} }
@@ -198,7 +232,7 @@
if (serversCount < limit.Amount) if (serversCount < limit.Amount)
{ {
if(int.TryParse(limit.ReadValue("cpu"), out int cpu) && if (int.TryParse(limit.ReadValue("cpu"), out int cpu) &&
int.TryParse(limit.ReadValue("memory"), out int memory) && int.TryParse(limit.ReadValue("memory"), out int memory) &&
int.TryParse(limit.ReadValue("disk"), out int disk)) int.TryParse(limit.ReadValue("disk"), out int disk))
{ {
@@ -221,4 +255,11 @@
} }
} }
} }
private async Task SelectImage(Image image)
{
Model.Image = image;
await InvokeAsync(StateHasChanged);
}
} }

View File

@@ -221,6 +221,8 @@
{ {
ServerGroups = (JsonConvert.DeserializeObject<ServerGroup[]>( ServerGroups = (JsonConvert.DeserializeObject<ServerGroup[]>(
User.ServerListLayoutJson) ?? Array.Empty<ServerGroup>()).ToList(); User.ServerListLayoutJson) ?? Array.Empty<ServerGroup>()).ToList();
await CheckServerGroups();
} }
foreach (var server in AllServers) foreach (var server in AllServers)
@@ -256,8 +258,8 @@
private async Task RemoveGroup(ServerGroup group) private async Task RemoveGroup(ServerGroup group)
{ {
ServerGroups.Remove(group); ServerGroups.Remove(group);
await EnsureAllServersInGroups();
await CheckServerGroups();
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init"); await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
@@ -266,11 +268,10 @@
private async Task SetEditMode(bool toggle) private async Task SetEditMode(bool toggle)
{ {
EditMode = toggle; EditMode = toggle;
await InvokeAsync(StateHasChanged);
if (EditMode) if (EditMode)
{ {
await EnsureAllServersInGroups(); await CheckServerGroups();
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init"); await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
@@ -278,6 +279,9 @@
else else
{ {
var json = JsonConvert.SerializeObject(await GetGroupsFromClient()); var json = JsonConvert.SerializeObject(await GetGroupsFromClient());
await CheckServerGroups();
User.ServerListLayoutJson = json; User.ServerListLayoutJson = json;
UserRepository.Update(User); UserRepository.Update(User);
@@ -297,62 +301,90 @@
Logger.Verbose("Server list group lenght too long"); Logger.Verbose("Server list group lenght too long");
return Array.Empty<ServerGroup>(); return Array.Empty<ServerGroup>();
} }
if (serverGroup.Servers.Any(x => AllServers.All(y => y.Id.ToString() != x)))
{
Logger.Verbose("User tried to add a server in his server list which he has no access to");
return Array.Empty<ServerGroup>();
}
} }
return serverGroups; return serverGroups;
} }
private Task EnsureAllServersInGroups() private Task CheckServerGroups()
{ {
var presentInGroup = new List<Server>(); var result = new List<ServerGroup>();
// Reconstruct the data with checking for invalid server ids
foreach (var group in ServerGroups) foreach (var group in ServerGroups)
{
var checkedGroup = new ServerGroup()
{
Name = group.Name
};
foreach (var server in group.Servers)
{
var s = AllServers.FirstOrDefault(x => x.Id.ToString() == server);
if (s != null) // This is a check for invalid server ids
{
checkedGroup.Servers.Add(s.Id.ToString());
}
}
result.Add(checkedGroup);
}
var presentInGroup = new List<Server>();
// Copy all servers to preset in group if they are in the users servers
foreach (var group in result)
{ {
foreach (var id in group.Servers) foreach (var id in group.Servers)
presentInGroup.Add(AllServers.First(x => x.Id.ToString() == id)); {
var s = AllServers.First(x => x.Id.ToString() == id);
presentInGroup.Add(s);
}
} }
var serversMissing = new List<Server>(); var serversMissing = new List<Server>();
// Make a list of missing servers
foreach (var server in AllServers) foreach (var server in AllServers)
{ {
if (presentInGroup.All(x => x.Id != server.Id)) if (presentInGroup.All(x => x.Id != server.Id))
serversMissing.Add(server); serversMissing.Add(server);
} }
// Add all missing servers into the default group
if (serversMissing.Any()) if (serversMissing.Any())
{ {
var defaultGroup = ServerGroups.FirstOrDefault(x => x.Name == ""); var defaultGroup = result.FirstOrDefault(x => x.Name == "");
if (defaultGroup == null) if (defaultGroup == null) // If group does not exist, create it
{ {
defaultGroup = new ServerGroup() defaultGroup = new ServerGroup()
{ {
Name = "" Name = ""
}; };
ServerGroups.Add(defaultGroup); result.Add(defaultGroup);
} }
foreach (var server in serversMissing) foreach (var server in serversMissing)
defaultGroup.Servers.Add(server.Id.ToString()); defaultGroup.Servers.Add(server.Id.ToString());
} }
ServerGroups = result;
return Task.CompletedTask; return Task.CompletedTask;
} }
private void AddStatus(Server server, string status) private void AddStatus(Server server, string status)
{ {
lock (StatusCache) lock (StatusCache)
{
if (!StatusCache.ContainsKey(server))
{ {
StatusCache.Add(server, status); StatusCache.Add(server, status);
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);
} }
} }
}
} }

View File

@@ -19,7 +19,7 @@ Add admin accounts;Admin Konto hinzufügen
First name;Vorname First name;Vorname
Last name;Nachname Last name;Nachname
Email address;E-Mail-Adresse Email address;E-Mail-Adresse
Enter password;Password eingeben Enter password;Passwort eingeben
Next;Weiter Next;Weiter
Back;Zurück Back;Zurück
Configure features;Features konfigurieren Configure features;Features konfigurieren
@@ -60,10 +60,10 @@ Servers;Server
Websites;Websiten Websites;Websiten
Databases;Datenbanken Databases;Datenbanken
Domains;Domains Domains;Domains
Changelog;Anderungen Changelog;Änderungen
Firstname;Vorname Firstname;Vorname
Lastname;Nachname Lastname;Nachname
Repeat password;Password wiederholen Repeat password;Passwort wiederholen
Sign Up;Anmelden Sign Up;Anmelden
Sign up to start with moonlight;Registrieren um mit Moonlight zu starten Sign up to start with moonlight;Registrieren um mit Moonlight zu starten
Sign up with Discord;Mit Discord Registrieren Sign up with Discord;Mit Discord Registrieren
@@ -71,11 +71,11 @@ Sign up with Google;Mit Google Registrieren
Sign-up;Registrieren Sign-up;Registrieren
Already registered?;Schon Registriert? Already registered?;Schon Registriert?
Sign in;Registrieren Sign in;Registrieren
Create something new;Etwas neues erstellen Create something new;Etwas Neues erstellen
Create a gameserver;Einen Gameserver erstellen Create a gameserver;Einen Gameserver erstellen
A new gameserver in just a few minutes;Ein neuer Gameserver in wenigen Minuten A new gameserver in just a few minutes;Ein neuer Gameserver in wenigen Minuten
Create a database;Eine Datenbank erstellen Create a database;Eine Datenbank erstellen
A quick way to store your data and manage it from all around the world;Eine schnelle Möglichkeit, um deine Daten von überall auf der Welt zu verwalten A quick way to store your data and manage it from all around the world;Eine schnelle Möglichkeit, deine Daten von überall auf der Welt zu verwalten
Manage your services;Deine Dienste verwalten Manage your services;Deine Dienste verwalten
Manage your gameservers;Gameserver verwalten Manage your gameservers;Gameserver verwalten
Adjust your gameservers;Deine Gameserver anpassen Adjust your gameservers;Deine Gameserver anpassen
@@ -100,7 +100,7 @@ aaPanel;aaPanel
Users;Benutzer Users;Benutzer
Support;Hilfe Support;Hilfe
Statistics;Statistiken Statistics;Statistiken
No nodes found. Start with adding a new node;Keine Nodes gefunden. Ein neues Node hinzufügen No nodes found. Start with adding a new node;Keine Nodes gefunden. Eine neue Node hinzufügen
Nodename;Nodename Nodename;Nodename
FQDN;FQDN FQDN;FQDN
Create;Erstellen Create;Erstellen
@@ -115,25 +115,25 @@ Memory;Arbeitsspeicher
Used / Available memory;Benutzter / Verfügbarer Arbeitsspeicher Used / Available memory;Benutzter / Verfügbarer Arbeitsspeicher
Storage;Speicherplatz Storage;Speicherplatz
Available storage;Verfügbarer Speicherplatz Available storage;Verfügbarer Speicherplatz
Add a new node;Ein neues Node hinzufügen Add a new node;Eine neue Node hinzufügen
Delete;Löschen Delete;Löschen
Deleting;Wirt gelöscht... Deleting;Wird gelöscht...
Edit;Bearbeiten Edit;Bearbeiten
Token Id;Token Id Token Id;Token ID
Token;Token Token;Token
Save;Speichern Save;Speichern
Setup;Aufsetzen Setup;Aufsetzen
Open a ssh connection to your node and enter;Eine SSH verbindung zum Node hinzufügen und öffnen Open a ssh connection to your node and enter;Eine SSH verbindung zu der Node hinzufügen und öffnen
and paste the config below. Then press STRG+O and STRG+X to save;und die Config darunter einfügern. Dann STRG+O und STRG+X um zu Speichern and paste the config below. Then press STRG+O and STRG+X to save;und die Config darunter einfügen. Dann STRG+O und STRG+X drücken um zu Speichern
Before configuring this node, install the daemon;Installiere den Daemon bevor du dieses Node konfigurierst Before configuring this node, install the daemon;Installiere den Daemon bevor du dieses Node konfigurierst
Delete this node?;Dieses Node löschen? Delete this node?;Diese Node löschen?
Do you really want to delete this node;Möchtest du dieses Node wirklich löschen? Do you really want to delete this node;Möchtest du diese Node wirklich löschen?
Yes;Ja Yes;Ja
No;Nein No;Nein
Status;Status Status;Status
Adding;Hinzufügen Adding;Hinzufügen
Port;Port Port;Port
Id;Id Id;ID
Manage;Verwalten Manage;Verwalten
Create new server;Neuen Server erstellen Create new server;Neuen Server erstellen
No servers found;Keine Server gefunden No servers found;Keine Server gefunden
@@ -150,20 +150,20 @@ Cores;Kerne
Owner;Besitzer Owner;Besitzer
Value;Wert Value;Wert
An unknown error occured;Ein unbekannter Fehler ist aufgetreten An unknown error occured;Ein unbekannter Fehler ist aufgetreten
No allocation found;Keine Zuweisung gefunden No allocation found;Keine Allocation gefunden
Identifier;Identifier Identifier;Identifier
UuidIdentifier;UuidIdentifier UuidIdentifier;UUIDIdentifier
Override startup command;Startup Befehl überschreiben Override startup command;Startup Befehl überschreiben
Loading;Wird geladen... Loading;Wird geladen...
Offline;Offline Offline;Offline
Connecting;Verbiden... Connecting;Verbiden...
Start;Start Start;Start
Restart;Neu Starten Restart;Neustarten
Stop;Stoppen Stop;Stoppen
Shared IP;Geteilte IP Shared IP;Geteilte IP
Server ID;Server ID Server ID;Server ID
Cpu;CPU Cpu;CPU
Console;Console Console;Konsole
Files;Dateien Files;Dateien
Backups;Backups Backups;Backups
Network;Netzwerk Network;Netzwerk
@@ -184,11 +184,11 @@ Search files and folders;Ordner und Dateien durchsuchen
Launch WinSCP;WinSCP starten Launch WinSCP;WinSCP starten
New folder;Neuer Ordner New folder;Neuer Ordner
Upload;Hochladen Upload;Hochladen
File name;Datei-Name File name;Dateiname
File size;Datei-Größe File size;Dateigröße
Last modified;Zuletz geändert Last modified;Zuletzt geändert
Cancel;Abbrechen Cancel;Abbrechen
Canceling;Wird Abbgebrochen Canceling;Wird Abgebrochen
Running;Läuft Running;Läuft
Loading backups;Backups werden geladen Loading backups;Backups werden geladen
Started backup creation;Backup wird erstellt Started backup creation;Backup wird erstellt
@@ -198,35 +198,35 @@ Move;Bewegen
Archive;Archivieren Archive;Archivieren
Unarchive;Archivieren rückgängig machen Unarchive;Archivieren rückgängig machen
Download;Herunterladen Download;Herunterladen
Starting download;Herunterladen wird gestartet Starting download;Download wird gestartet
Backup successfully created;Backup wurde erfolgreich erstellt Backup successfully created;Backup wurde erfolgreich erstellt
Restore;Wiederherstellen Restore;Wiederherstellen
Copy url;URL Kopieren Copy url;URL Kopieren
Backup deletion started;Backup löschung wird gestartet Backup deletion started;Backup Löschung wird gestartet
Backup successfully deleted;Backup wurde erfolgreich gelöscht Backup successfully deleted;Backup wurde erfolgreich gelöscht
Primary;Primärer Primary;Primärer
This feature is currently not available;Diese Funktion ist zur Zeit nicht verfügbar This feature is currently not available;Diese Funktion ist zurzeit leider nicht verfügbar
Send;Senden Send;Senden
Sending;Wird gesendet Sending;Wird gesendet
Welcome to the support chat. Ask your question here and we will help you;Willkommen in der Chat Hilfe. Stelle hier deine Frage und wir helfen dir. Welcome to the support chat. Ask your question here and we will help you;Willkommen im Support Chat. Stelle hier deine Frage und wir helfen dir.
minutes ago; Minuten minutes ago; Minuten her
just now;gerade eben just now;gerade eben
less than a minute ago;weniger als eine Minute less than a minute ago;vor weniger als einer Minute
1 hour ago;vor einer Stunde 1 hour ago;vor einer Stunde
1 minute ago;vor einer Minute 1 minute ago;vor einer Minute
Failed;Fehlgeschlagen Failed;Fehlgeschlagen
hours ago; Stunden hours ago; Stunden her
Open tickets;Tickets öffnen Open tickets;Tickets öffnen
Actions;Aktionen Actions;Aktionen
No support ticket is currently open;Kein Support Ticket ist zurzeit offen. No support ticket is currently open;Kein Support Ticket ist zurzeit offen.
User information;Benutzer-Information User information;Benutzer-Information
Close ticket;Ticket schließen Close ticket;Ticket schließen
Closing;Wird geschlossen... Closing;Wird geschlossen...
The support team has been notified. Please be patient;Das Support-Team wurde benachrichtigt. Habe etwas gedult The support team has been notified. Please be patient;Das Support-Team wurde benachrichtigt. Habe etwas Geduld
The ticket is now closed. Type a message to open it again;Das Ticket wurde geschlossen. Schreibe etwas, um es wieder zu öffnen The ticket is now closed. Type a message to open it again;Das Ticket wurde geschlossen. Schreibe etwas, um es wieder zu öffnen
1 day ago;vor einem Tag 1 day ago;vor einem Tag
is typing;schreibt... is typing;schreibt...
are typing;schreiben are typing;schreiben...
No domains available;Keine Domains verfügbar No domains available;Keine Domains verfügbar
Shared domains;Geteilte Domains Shared domains;Geteilte Domains
Shared domain;Geteilte Domain Shared domain;Geteilte Domain
@@ -238,16 +238,16 @@ Fetching dns records;Es wird nach DNS-Records gesucht
No dns records found;Keine DNS-Records gefunden No dns records found;Keine DNS-Records gefunden
Content;Inhalt Content;Inhalt
Priority;Priorität Priority;Priorität
Ttl;Ttl Ttl;TTL
Enable cloudflare proxy;Cloudflare-Proxy benutzen Enable cloudflare proxy;Cloudflare-Proxy benutzen
CF Proxy;CF Proxy CF Proxy;CF Proxy
days ago; Tage days ago; Tage her
Cancle;Abbrechen Cancle;Abbrechen
An unexpected error occured;Ein unbekannter Fehler ist aufgetreten An unexpected error occured;Ein unbekannter Fehler ist aufgetreten
Testy;Testy Testy;Testy
Error from cloudflare api;Fehler von der Cloudflare-API Error from cloudflare api;Fehler von der Cloudflare-API
Profile;Profil Profile;Profil
No subscription available;Kein Abo verfügbar No subscription available;Kein Abonnement verfügbar
Buy;Kaufen Buy;Kaufen
Redirecting;Weiterleiten Redirecting;Weiterleiten
Apply;Anwenden Apply;Anwenden
@@ -255,7 +255,7 @@ Applying code;Code Anwenden
Invalid subscription code;Unbekannter Abo-Code Invalid subscription code;Unbekannter Abo-Code
Cancel Subscription;Abo beenden Cancel Subscription;Abo beenden
Active until;Aktiv bis Active until;Aktiv bis
We will send you a notification upon subscription expiration;Wenn dein Abo endet, senden wir dir eine E-Mail We will send you a notification upon subscription expiration;Wenn dein Abonnement endet, senden wir dir eine E-Mail
This token has been already used;Dieser Token wurde schon benutzt This token has been already used;Dieser Token wurde schon benutzt
New login for;Neue Anmeldung für New login for;Neue Anmeldung für
No records found for this day;Für diesen Tag wurden keine Records gefunden No records found for this day;Für diesen Tag wurden keine Records gefunden
@@ -265,7 +265,7 @@ Minecraft version;Minecraft Version
Build version;Build Version Build version;Build Version
Server installation is currently running;Der Server wird installiert. Server installation is currently running;Der Server wird installiert.
Selected;Ausgewählt Selected;Ausgewählt
Move deleted;Gelöschtest Bewegen Move deleted;Gelöschtes Bewegen
Delete selected;Ausgewähltes löschen Delete selected;Ausgewähltes löschen
Log level;Log Level Log level;Log Level
Log message;Log Message Log message;Log Message
@@ -274,11 +274,11 @@ Version;Version
You are running moonlight version;Du benutzt die Moonlight-Version You are running moonlight version;Du benutzt die Moonlight-Version
Operating system;Betriebssystem Operating system;Betriebssystem
Moonlight is running on;Moonlight läuft auf Moonlight is running on;Moonlight läuft auf
Memory usage;Arbeitsspeicher-Auslastung Memory usage;Arbeitsspeicher Auslastung
Moonlight is using;Moonlight benutzt Moonlight is using;Moonlight benutzt
of memory;des Arbeitsspeichers of memory;des Arbeitsspeichers
Cpu usage;CPU Auslastung Cpu usage;CPU Auslastung
Refresh;Neu Laden Refresh;Neuladen
Send a message to all users;Eine Nachricht an alle Benutzer senden Send a message to all users;Eine Nachricht an alle Benutzer senden
IP;IP IP;IP
URL;URL URL;URL
@@ -286,9 +286,9 @@ Device;Gerät
Change url;URL Ändern Change url;URL Ändern
Message;Nachricht Message;Nachricht
Enter message;Nachricht eingeben Enter message;Nachricht eingeben
Enter the message to send;Eine Nachricht zum senden eingeben Enter the message to send;Eine Nachricht zum Senden eingeben
Confirm;Bestätigen Confirm;Bestätigen
Are you sure?;Bist du dir sicher Are you sure?;Bist du dir sicher?
Enter url;URL eingeben Enter url;URL eingeben
An unknown error occured while starting backup deletion;Ein unbekannter Fehler ist während der Backuplöschung aufgetreten An unknown error occured while starting backup deletion;Ein unbekannter Fehler ist während der Backuplöschung aufgetreten
Success;erfolgreich Success;erfolgreich
@@ -298,9 +298,9 @@ Backup successfully restored;Das Backup wurde erfolgreich wiedergeherstellt
Register for;Registrieren für Register for;Registrieren für
Core;Kern Core;Kern
Logs;Logs Logs;Logs
AuditLog;AuditLog AuditLog;Audit Log
SecurityLog;SecurityLog SecurityLog;Security Log
ErrorLog;ErrorLog ErrorLog;Error Log
Resources;Resourcen Resources;Resourcen
WinSCP cannot be launched here;WinSCP kann nicht gestartet werden WinSCP cannot be launched here;WinSCP kann nicht gestartet werden
Create a new folder;Neuen Ordner erstellen Create a new folder;Neuen Ordner erstellen
@@ -311,24 +311,24 @@ Sessions;Sitzungen
New user;Neuer Benutzer New user;Neuer Benutzer
Created at;Erstellt am Created at;Erstellt am
Mail template not found;E-Mail template wurde nicht gefunden Mail template not found;E-Mail template wurde nicht gefunden
Missing admin permissions. This attempt has been logged ;Fehlende Admin-Rechte. Dieser Versuch wurde aufgezeichnet Missing admin permissions. This attempt has been logged ;Fehlende Admin-Rechte. Dieser Versuch wurde aufgezeichnet und ist für das ganze Admin Team sichtbar
Address;Addresse Address;Addresse
City;Stadt City;Stadt
State;Land State;Land
Country;Staat Country;Staat
Totp;Totp Totp;TOTP
Discord;Discord Discord;Discord
Subscription;Abonament Subscription;Abonnement
None;None None;None
No user with this id found;Kein Benutzer mit dieser ID gefunden No user with this id found;Kein Benutzer mit dieser ID gefunden
Back to list;Zurück zur liste Back to list;Zurück zur Liste
New domain;Neue domain New domain;Neue Domain
Reset password;Password wiederherstellen Reset password;Password wiederherstellen
Password reset;Password wiederherstellung Password reset;Password wiederherstellung
Reset the password of your account;Password deines Accounts zurücksetzen Reset the password of your account;Password deines Accounts zurücksetzen
Wrong here?;Falsch hier? Wrong here?;Falsch hier?
A user with this email can not be found;Ein Benutzer mit dieser E-Mail konnte nicht gefunden werden A user with this email can not be found;Ein Benutzer mit dieser E-Mail konnte nicht gefunden werden
Passwort reset successfull. Check your mail;Password wiederherstellung erfolgreich. Überprüfe deine Email Passwort reset successfull. Check your mail;Password wiederherstellung erfolgreich. Überprüfe dein Email Postfach
Discord bot;Discord Bot Discord bot;Discord Bot
New image;Neues Image New image;Neues Image
Description;Beschreibung Description;Beschreibung
@@ -348,15 +348,15 @@ Startup detection;Startuperkennung
Stop command;Stopp-Befehl Stop command;Stopp-Befehl
Successfully saved image;Das Image wurde erfolgreich gespeichert Successfully saved image;Das Image wurde erfolgreich gespeichert
No docker images found;Keine Docker Images gefunden No docker images found;Keine Docker Images gefunden
Key;Schlüssel Key;Key
Default value;Standardwert Default value;Standardwert
Allocations;Zuweisung Allocations;Zuweisung
No variables found;Keine Variablen gefunden No variables found;Keine Variablen gefunden
Successfully added image;Das Image wurde erfolgreich hinzugefügt Successfully added image;Das Image wurde erfolgreich hinzugefügt
Password change for;Password ändern für Password change for;Password ändern für
of;von of;von
New node;Neues Node New node;Neue Node
Fqdn;Fqdn Fqdn;FQDN
Cores used;Kerne genutzt Cores used;Kerne genutzt
used;benutzt used;benutzt
5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64 5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64
@@ -367,7 +367,7 @@ details;Details
1;1 1;1
2;2 2;2
DDos;DDos DDos;DDos
No ddos attacks found;Keine DDoS gefunden No ddos attacks found;Keine DDos Attacken gefunden
Node;Node Node;Node
Date;Datum Date;Datum
DDos attack started;DDos Attacke gestartet DDos attack started;DDos Attacke gestartet
@@ -383,25 +383,25 @@ Do you really want to kill all running servers?;Möchtest du wirklich alle laufe
Change power state for;Power-State ändern für Change power state for;Power-State ändern für
to;zu to;zu
Stop all servers;Alle Server stoppen Stop all servers;Alle Server stoppen
Do you really want to stop all running servers?;Möchtest du wirklich alle laufenden Server Killen? Do you really want to stop all running servers?;Möchtest du wirklich alle laufenden Server stoppen?
Manage ;Verwalten Manage ;Verwalten
Manage user ;Benutzer verwalten Manage user ;Benutzer verwalten
Reloading;Neu Laden... Reloading;Lade neu...
Update;Aktualisieren Update;Aktualisieren
Updating;Wird Aktualisiert... Updating;Wird Aktualisiert...
Successfully updated user;Benutzer erfolgreich aktualisiert Successfully updated user;Benutzer erfolgreich aktualisiert
Discord id;Discord ID Discord id;Discord User ID
Discord username;Discord Benutzername Discord username;Discord Benutzername
Discord discriminator;Discord Tag Discord discriminator;Discord Tag
The Name field is required.;Der Name dieses Feldes ist erforderlich The Name field is required.;Der Name dieses Feldes ist erforderlich
An error occured while logging you in;Ein Fehler ist aufgetreten, als du dich angemeldet hast An error occured while logging you in;Ein Fehler ist aufgetreten, während du dich angemeldet hast
You need to enter an email address;Du musst eine E-Mail-Adresse angeben You need to enter an email address;Du musst eine E-Mail-Adresse angeben
You need to enter a password;Du musst ein Password eingeben You need to enter a password;Du musst ein Password eingeben
You need to enter a password with minimum 8 characters in lenght;Du musst ein Password eingeben, das mindestens 8 Buchstaben lang ist You need to enter a password with minimum 8 characters in lenght;Du musst ein Password eingeben, das mindestens 8 Buchstaben lang ist
Proccessing;Weid verarbeitet... Proccessing;Weid verarbeitet...
The FirstName field is required.;Das Vorname-Feld ist erforderlich The FirstName field is required.;Das Vorname-Feld ist erforderlich
The LastName field is required.;Das Nachname-Feld ist erforderlich The LastName field is required.;Das Nachname-Feld ist erforderlich
The Address field is required.;Das Address-Feld ist erforderlich The Address field is required.;Das Addresse-Feld ist erforderlich
The City field is required.;Das Stadt-Feld ist erforderlich The City field is required.;Das Stadt-Feld ist erforderlich
The State field is required.;Das Staat-Feld ist erforderlich The State field is required.;Das Staat-Feld ist erforderlich
The Country field is required.;Das Land-Feld ist erforderlich The Country field is required.;Das Land-Feld ist erforderlich
@@ -417,7 +417,7 @@ Disable;Deaktivieren
Addons;Add-ons Addons;Add-ons
Javascript version;Javascript Version Javascript version;Javascript Version
Javascript file;Javascript Datei Javascript file;Javascript Datei
Select javascript file to execute on start;Javascript Datei zum starten auswählen Select javascript file to execute on start;Javascript Datei zum Starten auswählen
Submit;Einreichen Submit;Einreichen
Processing;Wird verarbeitet... Processing;Wird verarbeitet...
Go up;Nach oben gehen Go up;Nach oben gehen
@@ -433,15 +433,15 @@ Are you sure you want to reset this server?;Möchtest du diesen Server wirklich
Are you sure? This cannot be undone;Bist du dir sicher? Dies kann nicht rückgängig gemacht werden Are you sure? This cannot be undone;Bist du dir sicher? Dies kann nicht rückgängig gemacht werden
Resetting server;Server wird zurückgesetzt... Resetting server;Server wird zurückgesetzt...
Deleted file;Datei gelöscht Deleted file;Datei gelöscht
Reinstalling server;Server wird reinstalliert Reinstalling server;Server wird neuinstalliert
Uploading files;Dateien wurden hochgeladen Uploading files;Dateien wurden hochgeladen
complete;vollständig complete;vollständig
Upload complete;Upload komplett Upload complete;Upload komplett
Security;Sicherheit Security;Sicherheit
Subscriptions;Abonaments Subscriptions;Abonnements
2fa Code;2FA Code 2fa Code;2FA Code
Your account is secured with 2fa;Dein Account wird mit 2-FA gesichert Your account is secured with 2fa;Dein Account wird mit 2-FA gesichert
anyone write a fancy text here?;hier einen schönen Text schreiben? anyone write a fancy text here?;hier einen schönen Text schreiben? -Nö.
Activate 2fa;2-FA Aktivieren Activate 2fa;2-FA Aktivieren
2fa apps;2-FA Apps 2fa apps;2-FA Apps
Use an app like ;Benutze eine App wie Use an app like ;Benutze eine App wie
@@ -453,34 +453,34 @@ Finish activation;Aktivierung fertig
New password;Neues Password New password;Neues Password
Secure your account;Deinen Account sichern Secure your account;Deinen Account sichern
2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.;2-FA fügt eine weitere Sicherheits-Schicht hinzu. Du musst einen 6-Ziffern-Code eingeben, um dich anzumelden. 2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.;2-FA fügt eine weitere Sicherheits-Schicht hinzu. Du musst einen 6-Ziffern-Code eingeben, um dich anzumelden.
New subscription;Neues Abonament New subscription;Neues Abonnement
You need to enter a name;Du musst einen Namen eingeben You need to enter a name;Du musst einen Namen eingeben
You need to enter a description;Du musst eine Beschreibung eigeben You need to enter a description;Du musst eine Beschreibung eigeben
Add new limit;Ein neues Limit hinzufügen Add new limit;Ein neues Limit hinzufügen
Create subscription;Abonament erstellen Create subscription;Abonnement erstellen
Options;Optionen Options;Optionen
Amount;Betrag Amount;Betrag
Do you really want to delete it?;Möchtes du es wirklich löschen? Do you really want to delete it?;Möchtes du es wirklich löschen?
Loading your subscription;Dein Abonament wird geladen Loading your subscription;Dein Abonnement wird geladen
Searching for deploy node;#Empty# Searching for deploy node;Suche nach einer verfügbaren Node
Searching for available images;Nach verfügbaren Images wird gesucht Searching for available images;Nach verfügbaren Images wird gesucht
Server details;Server Details Server details;Server Details
Configure your server;Deinen Server konfigurieren Configure your server;Deinen Server konfigurieren
Default;Standart Default;Standart
You reached the maximum amount of servers for every image of your subscription;Du hast die maximale Anzahl an Images von deinem Abonament erreicht. You reached the maximum amount of servers for every image of your subscription;Du hast die maximale Anzahl an Images von deinem Abonnement erreicht.
Personal information;Prsönliche Informationen Personal information;Persönliche Informationen
Enter code;Code eingeben Enter code;Code eingeben
Server rename;Server Umbenennen Server rename;Server Umbenennen
Create code;Code erstellen Create code;Code erstellen
Save subscription;Abonament speichern Save subscription;Abonnement speichern
Enter your information;Informationen eingeben Enter your information;Informationen eingeben
You need to enter your full name in order to use moonlight;Du musst deinen ganzen Namen eingeben, um Moonlight zu nutzen You need to enter your full name in order to use moonlight;Du musst deinen ganzen Namen eingeben, um Moonlight zu nutzen
No node found;Kein Node gefunden No node found;Kein Node gefunden
No node found to deploy to found;#Empty# No node found to deploy to found;Keine Node für die Bereitstellung gefunden
Node offline;Node offline Node offline;Node offline
The node the server is running on is currently offline;Das Node, auf dem der Server grat läuft, ist offline The node the server is running on is currently offline;Das Node, auf dem der Server grade läuft, ist offline
Server not found;Server konnte nicht gefunden werden Server not found;Server konnte nicht gefunden werden
A server with that id cannot be found or you have no access for this server;Ein Server mit dieser ID konnte nicht gefunden werden A server with that id cannot be found or you have no access for this server;Ein Server mit dieser ID konnte nicht gefunden werden oder du hast keinen Zugriff auf ihn
Compress;Komprimieren Compress;Komprimieren
Decompress;De-Komprimieren Decompress;De-Komprimieren
Moving;Bewegen... Moving;Bewegen...
@@ -495,11 +495,11 @@ No SSL certificate found;Keine SSL-Zertifikate gefunden
Ftp Host;FTP Host Ftp Host;FTP Host
Ftp Port;FTP Port Ftp Port;FTP Port
Ftp Username;FTP Username Ftp Username;FTP Username
Ftp Password;FTP Password Ftp Password;FTP Passwort
Use;Benutzen Use;Benutzen
SSL Certificates;SSL Zertifikate SSL Certificates;SSL Zertifikate
SSL certificates;SSL Zertifikate SSL certificates;SSL Zertifikate
Issue certificate;SSL-Zertifikat Ausgeben Issue certificate;SSL-Zertifikat erstellen lassen
New plesk server;Neuer Plesk Server New plesk server;Neuer Plesk Server
Api url;API URL Api url;API URL
Host system offline;Host System Offline Host system offline;Host System Offline
@@ -514,14 +514,14 @@ Username;Benutzername
SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one;SRV Records können aufgrund von Cloudflare nicht geupdatet werden. Bitte lösche den Record und erstelle einen neuen. SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one;SRV Records können aufgrund von Cloudflare nicht geupdatet werden. Bitte lösche den Record und erstelle einen neuen.
The User field is required.;Das Benutzer-Feld ist erforderlich The User field is required.;Das Benutzer-Feld ist erforderlich
You need to specify a owner;Du musst einen Server-Besitzer angeben You need to specify a owner;Du musst einen Server-Besitzer angeben
You need to specify a image;You need to specify a image You need to specify a image;Du musst ein Image angeben
Api Url;API URL Api Url;API URL
Api Key;Api Key Api Key;Api Key
Duration;Dauer Duration;Dauer
Enter duration of subscription;Dauer des Abonaments eingeben Enter duration of subscription;Dauer des Abonnements eingeben
Copied code to clipboard;Code in die Zwischenablage kopiert Copied code to clipboard;Code in die Zwischenablage kopiert
Invalid or expired subscription code;Ungültiger oder Abgelaufener Abo-Code Invalid or expired subscription code;Ungültiger oder Abgelaufener Abo-Code
Current subscription;Dein Abonament Current subscription;Dein Abonnement
You need to specify a server image;Du musst ein Image angeben You need to specify a server image;Du musst ein Image angeben
CPU;CPU CPU;CPU
Hour;Stunde Hour;Stunde
@@ -532,14 +532,14 @@ All time;Für immer
This function is not implemented;Diese Funktion wurde noch nicht hinzugefügt This function is not implemented;Diese Funktion wurde noch nicht hinzugefügt
Domain details;Domain Details Domain details;Domain Details
Configure your domain;Deine Domain konfigurieren Configure your domain;Deine Domain konfigurieren
You reached the maximum amount of domains in your subscription;Du hast das Maximum an Domains in deinem Abonament erreicht You reached the maximum amount of domains in your subscription;Du hast das Maximum an Domains in deinem Abonnement erreicht
You need to specify a shared domain;Du musst eine Shared-Domain angeben You need to specify a shared domain;Du musst eine Shared-Domain angeben
A domain with this name does already exist for this shared domain;Eine Domain mit diesem Name existiert bereits in dieser Shared-Domain A domain with this name does already exist for this shared domain;Eine Domain mit diesem Name existiert bereits in dieser Shared-Domain
The Email field is required.;Das E-Mail-Feld ist erforderlich The Email field is required.;Das E-Mail-Feld ist erforderlich
The Password field is required.;Das Password-Feld ist erforderlich The Password field is required.;Das Password-Feld ist erforderlich
The ConfirmPassword field is required.;Das Password-Bestätigen-Feld ist erforderlich The ConfirmPassword field is required.;Das Password-Bestätigen-Feld ist erforderlich
Passwords need to match;Die Passwörter müssen übereinstimmen Passwords need to match;Die Passwörter müssen übereinstimmen
Cleanup exception;Cleanup ausnahme Cleanup exception;Cleanup Ausnahme
No shared domain found;Keine Shared-Domain gefunden No shared domain found;Keine Shared-Domain gefunden
Searching for deploy plesk server;Suchen um den Plesk Server aufzusetzen Searching for deploy plesk server;Suchen um den Plesk Server aufzusetzen
No plesk server found;Kein Plesk Server gefunden No plesk server found;Kein Plesk Server gefunden
@@ -561,7 +561,7 @@ We were not able to find any domains associated with your account;Wir haben kein
You have no websites;Du hast keine Websites You have no websites;Du hast keine Websites
We were not able to find any websites associated with your account;Wir haben keine Websiten, die mit deinem Account verbunden sind, gefunden We were not able to find any websites associated with your account;Wir haben keine Websiten, die mit deinem Account verbunden sind, gefunden
Guest;Gast Guest;Gast
You need a domain;Du brauchts eine Domain You need a domain;Du brauchst eine Domain
New post;Neuer Post New post;Neuer Post
New entry;Neuer Eintrag New entry;Neuer Eintrag
You have no servers;Du hast keine Server You have no servers;Du hast keine Server
@@ -572,27 +572,27 @@ Error from daemon;Fehler vom Daemon
End;Ende End;Ende
Cloud panel;Cloud Panel Cloud panel;Cloud Panel
Cloud panels;Cloud Panels Cloud panels;Cloud Panels
New cloud panel;Neues cloud Panel New cloud panel;Neues Cloud Panel
You need to enter an api key;Du musst einen API-Key eigeben You need to enter an api key;Du musst einen API-Key eigeben
Webspaces;Webspaces Webspaces;Webspaces
New webspace;Neuer Webspace New webspace;Neuer Webspace
The uploaded file should not be bigger than 100MB;DIe Datei sollte nicht größer als 100MB sein The uploaded file should not be bigger than 100MB;Die Datei sollte nicht größer als 100MB sein
An unknown error occured while uploading a file;Ein unbekannter Fehler ist während dem Datei-Hochladen aufgetreten An unknown error occured while uploading a file;Ein unbekannter Fehler ist während dem Datei Upload aufgetreten
No databases found for this webspace;Keine Datenbanken für diesen Webspace gefunden No databases found for this webspace;Keine Datenbanken für diesen Webspace gefunden
Sftp;SFTP Sftp;SFTP
Sftp Host;Sftp Host Sftp Host;SFTP Host
Sftp Port;Sftp Port Sftp Port;SFTP Port
Sftp Username;Sftp Benutzername Sftp Username;SFTP Benutzername
Sftp Password;Sftp Password Sftp Password;SFTP Password
Lets Encrypt certificate successfully issued;Lets Encrypt Zertifikat erfolgreich erstellt Lets Encrypt certificate successfully issued;Lets Encrypt Zertifikat erfolgreich erstellt
Add shared domain;Shared Domain Hinzufügen Add shared domain;Shared Domain Hinzufügen
Webspace;Webspace Webspace;Webspace
You reached the maximum amount of websites in your subscription;Du hast das Maximum an Websiten in deinem Abonament erreicht You reached the maximum amount of websites in your subscription;Du hast das Maximum an Websiten in deinem Abonnement erreicht
Searching for deploy web host;Suchen um den Webhost aufzusetzen Searching for deploy web host;Suchen um den Webhost aufzusetzen
Webspace details;Webspace Details Webspace details;Webspace Details
Web host;Web host Web host;Web host
Configure your webspaces;Konfiguriere deine Webspaces Configure your webspaces;Konfiguriere deine Webspaces
You reached the maximum amount of webspaces in your subscription;Du hast das Maximum an Webspaces in deinem Abonament erreicht You reached the maximum amount of webspaces in your subscription;Du hast das Maximum an Webspaces in deinem Abonnement erreicht
Create a webspace;Einen Webspace erstellen Create a webspace;Einen Webspace erstellen
Manage your webspaces;Deine Webspaces verwalten Manage your webspaces;Deine Webspaces verwalten
Modify the content of your webspaces;Den Inhalt deiner Webspaces verwalten Modify the content of your webspaces;Den Inhalt deiner Webspaces verwalten
@@ -607,11 +607,11 @@ We paused your connection because of inactivity. The resume just focus the tab a
Failed to reconnect to the moonlight servers;Die Wiederverbindung zu den Moonlight-Servern ist gescheitert Failed to reconnect to the moonlight servers;Die Wiederverbindung zu den Moonlight-Servern ist gescheitert
We were unable to reconnect to moonlight. Please refresh the page;Verbindung zu Moonlight fehlgeschlagen. Bitte aktualisiere die Seite We were unable to reconnect to moonlight. Please refresh the page;Verbindung zu Moonlight fehlgeschlagen. Bitte aktualisiere die Seite
Failed to reconnect to the moonlight servers. The connection has been rejected;Die Wiederverbindung zu den Moonlight-Servern ist fehlgeschlagen. Die Verbindung wurde abgelehnt Failed to reconnect to the moonlight servers. The connection has been rejected;Die Wiederverbindung zu den Moonlight-Servern ist fehlgeschlagen. Die Verbindung wurde abgelehnt
We were unable to reconnect to moonlight. Most of the time this is caused by an update of moonlight. Please refresh the page;Verbindung zu Moonlight fehlgeschlagen. Meistens wird dies durch eine Aktualisierung von Moonlight verursacht. Bitte aktualisieren Sie die Seite We were unable to reconnect to moonlight. Most of the time this is caused by an update of moonlight. Please refresh the page;Verbindung zu Moonlight fehlgeschlagen. Meistens wird dies durch eine Aktualisierung von Moonlight verursacht. Bitte aktualisiere die Seite
Verifying token, loading user data;Token verifizieren, Benutzer-Daten laden Verifying token, loading user data;Token verifizieren, lade Benutzerdaten
Reload config;Konfiguration neu laden Reload config;Konfiguration neuladen
Successfully reloading configuration;Konfiguration wird neu geladen... Successfully reloading configuration;Konfiguration wird neugeladen...
Successfully reloaded configuration;Die Konfiguration wurde erfolgreich neu geladen Successfully reloaded configuration;Die Konfiguration wurde erfolgreich neugeladen
Flows;Flows Flows;Flows
Add node;Node Hinzufügen Add node;Node Hinzufügen
Web system;Web System Web system;Web System
@@ -627,10 +627,10 @@ Fabric loader version;Fabric Loader Version
Rate;Rate Rate;Rate
Hey, can i borrow you for a second?;Hey, kann ich dich mal kurz ausleihen? Hey, can i borrow you for a second?;Hey, kann ich dich mal kurz ausleihen?
We want to improve our services and get a little bit of feedback how we are currently doing. Please leave us a rating;Da wir unsere Dienste ständig verbessern, möchten wir dich um Feedback bitten. Bitte Bewerte uns: We want to improve our services and get a little bit of feedback how we are currently doing. Please leave us a rating;Da wir unsere Dienste ständig verbessern, möchten wir dich um Feedback bitten. Bitte Bewerte uns:
Thanks for your rating;Danke für deine Bewertun Thanks for your rating;Danke für deine Bewertung
It would be really kind of you rating us on a external platform as it will help our project very much;Es wäre wirklich nett, wenn du uns auf einer externen Plattform bewerten würdest, denn das würde unserem Projekt sehr helfen It would be really kind of you rating us on a external platform as it will help our project very much;Es wäre wirklich nett, wenn du uns auf einer externen Plattform bewerten würdest, denn das würde unserem Projekt sehr helfen
Close;Schließen Close;Schließen
Rating saved;Bewretung gespeichert Rating saved;Bewertung gespeichert
Group;Gruppe Group;Gruppe
Beta;Beta Beta;Beta
Create a new group;Eine neue Gruppe erstellen Create a new group;Eine neue Gruppe erstellen

View File

@@ -41,6 +41,7 @@
.components-reconnect-rejected > .rejected { .components-reconnect-rejected > .rejected {
display: block; display: block;
} }
.components-reconnect-hide > div { .components-reconnect-hide > div {
display: none; display: none;
} }
@@ -56,3 +57,16 @@
.components-reconnect-rejected { .components-reconnect-rejected {
display: block; display: block;
} }
::-webkit-scrollbar {
width: 12px;
background: var(--kt-card-bg);
}
::-webkit-scrollbar-thumb {
background: #6964E4;
}
::-webkit-scrollbar-thumb:hover {
background: #6964E4;
}

View File

@@ -22,7 +22,7 @@
![Screen Shot](https://media.discordapp.net/attachments/1059911407170228234/1119793539732217876/image.png?width=1340&height=671) ![Screen Shot](https://media.discordapp.net/attachments/1059911407170228234/1119793539732217876/image.png?width=1340&height=671)
Moonlight is a new free and open source alternative to pterodactyl allowing users to create their own hosting platform and host all sorts of gameservers in docker containers. With a simple migration from pterodactyl to moonlight (migrator will be open source soon) you can easily switch to moonlight and use its features like a server manager, cleanup system and automatic version switcher, just to name a few. Moonlight is a new free and open source alternative to pterodactyl allowing users to create their own hosting platform and host all sorts of gameservers in docker containers. With a simple migration from pterodactyl to moonlight ([see guide](https://docs.moonlightpanel.xyz/migrating-from-pterodactyl)) you can easily switch to moonlight and use its features like a server manager, cleanup system and automatic version switcher, just to name a few.
Moonlight's core features are Moonlight's core features are